@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.
- package/README.md +180 -61
- package/dist/chains/index.d.ts +17 -17
- package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
- package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
- package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
- package/dist/entity/index.cjs +4 -10
- package/dist/entity/index.d.cts +3 -3
- package/dist/entity/index.d.ts +3 -3
- package/dist/entity/index.js +3 -4
- package/dist/entity-BM1unrQf.cjs +41 -0
- package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
- package/dist/entity-CFAI1KyG.js +36 -0
- package/dist/entity-CFAI1KyG.js.map +1 -0
- package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
- package/dist/entity-CGqx_AU0.js.map +1 -0
- package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
- package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
- package/dist/expirationTime-CkIGNAg8.cjs +140 -0
- package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
- package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
- package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
- package/dist/expirationTime-UKPuA-LM.js.map +1 -0
- package/dist/expiry-B48I-yTA.d.cts +40 -0
- package/dist/expiry-B48I-yTA.d.cts.map +1 -0
- package/dist/expiry-S2PUL8Hg.d.ts +40 -0
- package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
- package/dist/index-C-mMF5WZ.d.cts +735 -0
- package/dist/index-C-mMF5WZ.d.cts.map +1 -0
- package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
- package/dist/index-C4zZqJ9S.d.ts.map +1 -0
- package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
- package/dist/index-CcVWYB6v.d.cts.map +1 -0
- package/dist/index-DCx2XC6S.d.ts +489 -0
- package/dist/index-DCx2XC6S.d.ts.map +1 -0
- package/dist/index-DLb7BfIw.d.ts +735 -0
- package/dist/index-DLb7BfIw.d.ts.map +1 -0
- package/dist/index-DzriOUm8.d.cts +489 -0
- package/dist/index-DzriOUm8.d.cts.map +1 -0
- package/dist/index.cjs +162 -146
- package/dist/index.d.cts +180 -133
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +180 -133
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +93 -83
- package/dist/index.js.map +1 -1
- package/dist/params-BuIrJA9a.cjs +45 -0
- package/dist/params-DDJOnsNi.js +22 -0
- package/dist/params-DDJOnsNi.js.map +1 -0
- package/dist/query/index.cjs +9 -4
- package/dist/query/index.d.cts +6 -6
- package/dist/query/index.d.ts +6 -6
- package/dist/query/index.js +4 -5
- package/dist/query-D0KtuFh2.cjs +1625 -0
- package/dist/query-DpRDozmj.js +1417 -0
- package/dist/query-DpRDozmj.js.map +1 -0
- package/dist/schema/index.cjs +46 -0
- package/dist/schema/index.d.cts +2 -0
- package/dist/schema/index.d.ts +2 -0
- package/dist/schema/index.js +3 -0
- package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
- package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
- package/dist/schema-DY4sqehl.js.map +1 -0
- package/dist/types/index.cjs +1 -1
- package/dist/types/index.d.cts +5 -5
- package/dist/types/index.d.ts +5 -5
- package/dist/types/index.js +1 -1
- package/dist/utils/index.cjs +3 -5
- package/dist/utils/index.d.cts +3 -3
- package/dist/utils/index.d.ts +3 -3
- package/dist/utils/index.js +3 -3
- package/package.json +9 -12
- package/src/actions/public/getEntity.test.ts +69 -0
- package/src/actions/public/getEntity.ts +34 -20
- package/src/actions/public/predictEntityKeys.ts +8 -7
- package/src/actions/public/query.test.ts +58 -0
- package/src/actions/public/query.ts +41 -13
- package/src/actions/wallet/createEntity.ts +34 -63
- package/src/actions/wallet/executeBatch.test.ts +38 -0
- package/src/actions/wallet/executeBatch.ts +26 -9
- package/src/actions/wallet/extendEntity.ts +9 -8
- package/src/actions/wallet/patchEntity.ts +43 -52
- package/src/clients/createPublicClient.test.ts +10 -2
- package/src/clients/createWalletClient.ts +4 -1
- package/src/clients/decorators/arkivPublic.ts +123 -80
- package/src/clients/decorators/arkivWallet.ts +113 -65
- package/src/entity/create.test.ts +25 -21
- package/src/entity/expiry.ts +8 -6
- package/src/entity/flags.ts +1 -1
- package/src/entity/index.ts +2 -4
- package/src/entity/key.ts +3 -3
- package/src/entity/operations.ts +1 -1
- package/src/errors.ts +2 -11
- package/src/index.ts +18 -6
- package/src/query/builderBase.ts +166 -0
- package/src/query/countBuilder.test.ts +91 -0
- package/src/query/countBuilder.ts +67 -0
- package/src/query/engine.ts +9 -3
- package/src/query/errors.ts +24 -14
- package/src/query/expression.test.ts +129 -21
- package/src/query/expression.ts +323 -94
- package/src/query/index.ts +26 -4
- package/src/query/injection.test.ts +5 -11
- package/src/query/queryBuilder.test.ts +467 -58
- package/src/query/queryBuilder.ts +191 -98
- package/src/query/queryResult.ts +35 -9
- package/src/query/selection.test.ts +278 -24
- package/src/query/selection.ts +427 -86
- package/src/schema/attributes.test.ts +266 -0
- package/src/schema/attributes.ts +212 -0
- package/src/{attr → schema}/codec.test.ts +37 -5
- package/src/{attr → schema}/codec.ts +40 -11
- package/src/schema/column.ts +193 -0
- package/src/{attr → schema}/errors.ts +67 -10
- package/src/schema/index.ts +76 -0
- package/src/{attr → schema}/names.ts +4 -2
- package/src/schema/schema.test.ts +171 -0
- package/src/schema/schema.ts +250 -0
- package/src/{attr → schema}/types.ts +36 -30
- package/src/{attr → schema}/values.test.ts +62 -3
- package/src/{attr → schema}/values.ts +246 -65
- package/src/types/entity.test.ts +1 -44
- package/src/types/entity.ts +9 -55
- package/src/types/events.ts +1 -1
- package/src/types/index.ts +11 -4
- package/src/types/rpcSchema.ts +22 -16
- package/src/utils/arkivTransactions.test.ts +356 -116
- package/src/utils/arkivTransactions.ts +30 -38
- package/src/utils/entities.ts +10 -9
- package/src/utils/expirationTime.ts +5 -5
- package/src/utils/index.ts +0 -1
- package/src/utils/logger.test.ts +45 -0
- package/src/utils/logger.ts +5 -39
- package/src/utils/revert.test.ts +25 -0
- package/src/utils/revert.ts +28 -3
- package/dist/attr/index.cjs +0 -34
- package/dist/attr/index.d.cts +0 -2
- package/dist/attr/index.d.ts +0 -2
- package/dist/attr/index.js +0 -3
- package/dist/attr-tmyREHtw.js.map +0 -1
- package/dist/entity-B0t6sN7F.cjs +0 -71
- package/dist/entity-BIp_df9u.js +0 -67
- package/dist/entity-BIp_df9u.js.map +0 -1
- package/dist/entity-CniBgBSd.js.map +0 -1
- package/dist/expiry-BxOlvVWC.d.ts +0 -79
- package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
- package/dist/expiry-CG9GhVXv.cjs +0 -135
- package/dist/expiry-Cgdb_dbq.js.map +0 -1
- package/dist/expiry-DRV29F1r.d.cts +0 -79
- package/dist/expiry-DRV29F1r.d.cts.map +0 -1
- package/dist/flags-DCD26uhe.cjs +0 -43
- package/dist/flags-DUT2gHEU.js +0 -33
- package/dist/flags-DUT2gHEU.js.map +0 -1
- package/dist/index-BkqSnGJH.d.ts.map +0 -1
- package/dist/index-C7fbakoI.d.cts +0 -278
- package/dist/index-C7fbakoI.d.cts.map +0 -1
- package/dist/index-CIcHPx2j.d.cts +0 -424
- package/dist/index-CIcHPx2j.d.cts.map +0 -1
- package/dist/index-CLhsFyVH.d.cts.map +0 -1
- package/dist/index-Cd2aaWme.d.ts +0 -424
- package/dist/index-Cd2aaWme.d.ts.map +0 -1
- package/dist/index-Cq6fa8AI.d.ts +0 -278
- package/dist/index-Cq6fa8AI.d.ts.map +0 -1
- package/dist/index-DaNeJUw8.d.ts +0 -955
- package/dist/index-DaNeJUw8.d.ts.map +0 -1
- package/dist/index-Dv16LO2s.d.cts +0 -955
- package/dist/index-Dv16LO2s.d.cts.map +0 -1
- package/dist/payload-AfUfSctb.d.ts.map +0 -1
- package/dist/payload-D167BlJR.d.cts.map +0 -1
- package/dist/payload-DE7wMztL.js +0 -92
- package/dist/payload-DE7wMztL.js.map +0 -1
- package/dist/payload-DqFweqiO.cjs +0 -108
- package/dist/query-9oojUdM7.cjs +0 -1128
- package/dist/query-Ber-lOJQ.js +0 -968
- package/dist/query-Ber-lOJQ.js.map +0 -1
- package/src/actions/public/getEntityCount.ts +0 -10
- package/src/attr/attributes.test.ts +0 -120
- package/src/attr/attributes.ts +0 -196
- package/src/attr/index.ts +0 -50
- package/src/types/mimeTypes.ts +0 -27
- package/src/utils/compression.test.ts +0 -32
- package/src/utils/compression.ts +0 -102
- package/src/utils/payload.ts +0 -36
- /package/src/{attr → schema}/decimal.ts +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { describe, expect, it } from "bun:test"
|
|
2
2
|
import { decodeAbiParameters, parseAbiParameters, toHex } from "viem"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { encodeCreate } from "../schema/attributes"
|
|
4
|
+
import { defineSchema } from "../schema/schema"
|
|
5
|
+
import { bytes, dec, i32, str, u256 } from "../schema/values"
|
|
5
6
|
import { InvalidCreationFlagsError } from "./errors"
|
|
6
7
|
import { decodeCreationFlags, encodeCreationFlags } from "./flags"
|
|
7
8
|
import { MAX_SALT, NO_SALT, predictEntityKey, randomSalt, resolveSalt, validateSalt } from "./key"
|
|
@@ -148,9 +149,10 @@ describe("the Create operation", () => {
|
|
|
148
149
|
})
|
|
149
150
|
|
|
150
151
|
it("carries attributes as (name, typeId, bytes), sorted ascending by name", () => {
|
|
152
|
+
const schema = defineSchema({ zebra: i32(), apple: str(), mango: u256() })
|
|
151
153
|
const create = decodeCreate(
|
|
152
154
|
build({
|
|
153
|
-
attributes:
|
|
155
|
+
attributes: encodeCreate(schema, { zebra: i32(1), apple: str("x"), mango: u256(2n) }),
|
|
154
156
|
}).operationData,
|
|
155
157
|
)
|
|
156
158
|
const names = create.attributes.map((a) => a.name)
|
|
@@ -158,38 +160,40 @@ describe("the Create operation", () => {
|
|
|
158
160
|
expect(create.attributes.map((a) => a.typeId)).toEqual([8, 4, 2]) // apple str, mango u256, zebra i32
|
|
159
161
|
})
|
|
160
162
|
|
|
161
|
-
it("
|
|
163
|
+
it("sets bit 7 of the typeId on indexed columns, and only on them", () => {
|
|
164
|
+
const schema = defineSchema({ level: i32().indexed(), note: str(), body: bytes() })
|
|
162
165
|
const create = decodeCreate(
|
|
163
166
|
build({
|
|
164
|
-
attributes:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
167
|
+
attributes: encodeCreate(schema, {
|
|
168
|
+
level: 10,
|
|
169
|
+
note: "hi",
|
|
170
|
+
body: new Uint8Array([0xde, 0xad]),
|
|
171
|
+
}),
|
|
168
172
|
}).operationData,
|
|
169
173
|
)
|
|
170
174
|
const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a]))
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
expect(
|
|
174
|
-
expect(create.attributes[1].name).toBe(toHex("$payload", { size: 32 }))
|
|
175
|
-
|
|
176
|
-
expect(byName[toHex("$payload", { size: 32 })]).toMatchObject({ typeId: 7, value: "0xdead" })
|
|
177
|
-
expect(byName[toHex("$contentType", { size: 32 })].typeId).toBe(8)
|
|
178
|
-
expect(byName[toHex("level", { size: 32 })].typeId).toBe(2)
|
|
175
|
+
expect(byName[toHex("level", { size: 32 })].typeId).toBe(0x80 | 2)
|
|
176
|
+
expect(byName[toHex("note", { size: 32 })].typeId).toBe(8)
|
|
177
|
+
expect(byName[toHex("body", { size: 32 })]).toMatchObject({ typeId: 7, value: "0xdead" })
|
|
179
178
|
})
|
|
180
179
|
|
|
181
|
-
it("
|
|
180
|
+
it("carries no cells at all for an entity with no columns", () => {
|
|
182
181
|
const create = decodeCreate(
|
|
183
|
-
build({ attributes:
|
|
182
|
+
build({ attributes: encodeCreate(defineSchema({}), {}) }).operationData,
|
|
184
183
|
)
|
|
185
|
-
expect(create.attributes).toHaveLength(
|
|
186
|
-
expect(create.attributes[0].name).toBe(toHex("level", { size: 32 }))
|
|
184
|
+
expect(create.attributes).toHaveLength(0)
|
|
187
185
|
})
|
|
188
186
|
|
|
189
187
|
it("encodes each value type at its documented width", () => {
|
|
188
|
+
const schema = defineSchema({ n: i32(), big: u256(), d: dec(), s: str() })
|
|
190
189
|
const create = decodeCreate(
|
|
191
190
|
build({
|
|
192
|
-
attributes:
|
|
191
|
+
attributes: encodeCreate(schema, {
|
|
192
|
+
n: i32(-42),
|
|
193
|
+
big: u256(1n),
|
|
194
|
+
d: dec("1.5"),
|
|
195
|
+
s: str("hi"),
|
|
196
|
+
}),
|
|
193
197
|
}).operationData,
|
|
194
198
|
)
|
|
195
199
|
const byName = Object.fromEntries(create.attributes.map((a) => [a.name, a.value]))
|
package/src/entity/expiry.ts
CHANGED
|
@@ -50,7 +50,7 @@ export function makeExpiry(minLifetime: bigint, expiresAt: bigint | Date): Expir
|
|
|
50
50
|
return { minLifetime, expiresAt } as Expiry
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
/** The two `
|
|
53
|
+
/** The two `uint64` values the ABI carries, with every `Date` already placed. */
|
|
54
54
|
export type EncodedExpiry = {
|
|
55
55
|
/** Absolute target block height, or `0n` for a purely relative lifetime. */
|
|
56
56
|
expiresAt: bigint
|
|
@@ -72,10 +72,12 @@ export type ExpiryContext = {
|
|
|
72
72
|
/**
|
|
73
73
|
* Resolves an expiry into the wire pair and the block the entity will expire at.
|
|
74
74
|
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
75
|
+
* A duration arrives here already in blocks — {@link ExpirationTime} converted it at the nominal
|
|
76
|
+
* `BLOCK_TIME` (2 seconds) through {@link toBlocks} — and a `Date` deadline is placed against
|
|
77
|
+
* `currentBlock` at that same block time here. Both are **approximate**: block production is not a
|
|
78
|
+
* clock, so a lifetime asked for in days drifts if the chain runs fast or slow. The block the
|
|
79
|
+
* expiry resolves to against `currentBlock` comes back as {@link ResolvedExpiry.target}, so the
|
|
80
|
+
* approximation is always observable rather than assumed.
|
|
79
81
|
*
|
|
80
82
|
* @throws {InvalidExpiryError} If the expiry is malformed, does not fit the `uint64` the wire
|
|
81
83
|
* carries, or would leave the entity dead on arrival.
|
|
@@ -85,7 +87,7 @@ export function resolveExpiry(expiry: Expiry, context: ExpiryContext): ResolvedE
|
|
|
85
87
|
|
|
86
88
|
if (typeof expiry?.minLifetime !== "bigint" || expiry.expiresAt === undefined) {
|
|
87
89
|
throw new InvalidExpiryError(
|
|
88
|
-
"
|
|
90
|
+
"expiration must be built with ExpirationTime, e.g. ExpirationTime.fromDays(30) or " +
|
|
89
91
|
"ExpirationTime.atBlock(1_200_000n)",
|
|
90
92
|
)
|
|
91
93
|
}
|
package/src/entity/flags.ts
CHANGED
|
@@ -12,7 +12,7 @@ const PERMISSIONLESS_EXTENSION_BIT = 0b0000_0010
|
|
|
12
12
|
*/
|
|
13
13
|
export type CreationFlags = {
|
|
14
14
|
/**
|
|
15
|
-
* The entity's attributes
|
|
15
|
+
* The entity's attributes can never be changed — only its expiry extended, its
|
|
16
16
|
* ownership transferred, and the entity deleted.
|
|
17
17
|
*/
|
|
18
18
|
readonly?: boolean
|
package/src/entity/index.ts
CHANGED
|
@@ -6,10 +6,8 @@ export {
|
|
|
6
6
|
} from "./errors"
|
|
7
7
|
export type { EntityEventName } from "./events"
|
|
8
8
|
export { ENTITY_EVENTS_ABI } from "./events"
|
|
9
|
-
export type {
|
|
10
|
-
export { resolveExpiry, toBlocks } from "./expiry"
|
|
9
|
+
export type { Expiry, Lifetime } from "./expiry"
|
|
11
10
|
export type { CreationFlags, ResolvedCreationFlags } from "./flags"
|
|
12
|
-
export { decodeCreationFlags, encodeCreationFlags } from "./flags"
|
|
13
11
|
export type { SaltInput } from "./key"
|
|
14
12
|
export { MAX_SALT, NO_SALT, predictEntityKey, randomSalt } from "./key"
|
|
15
|
-
export { MAX_EXPIRES_AT
|
|
13
|
+
export { MAX_EXPIRES_AT } from "./params"
|
package/src/entity/key.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Address, encodePacked, type Hex, keccak256 } from "viem"
|
|
2
|
-
import type {
|
|
2
|
+
import type { CreateEntityOptions } from "../actions/wallet/createEntity"
|
|
3
3
|
import { ARKIV_ADDRESS } from "../consts"
|
|
4
4
|
import { InvalidSaltError, NoRandomSourceError } from "./errors"
|
|
5
5
|
|
|
@@ -7,7 +7,7 @@ import { InvalidSaltError, NoRandomSourceError } from "./errors"
|
|
|
7
7
|
export const MAX_SALT = 2n ** 128n - 1n
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
|
-
* Opts a create out of salting, as {@link
|
|
10
|
+
* Opts a create out of salting, as {@link CreateEntityOptions.salt}: the key is then derived
|
|
11
11
|
* from the owner and its nonce alone, so anyone who knows both can work it out before the create
|
|
12
12
|
* lands.
|
|
13
13
|
*/
|
|
@@ -19,7 +19,7 @@ export const NO_SALT = Symbol("NO_SALT")
|
|
|
19
19
|
export type SaltInput = bigint | typeof NO_SALT
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* A cryptographically random 128-bit salt — what {@link
|
|
22
|
+
* A cryptographically random 128-bit salt — what {@link CreateEntityOptions.salt} defaults to.
|
|
23
23
|
*
|
|
24
24
|
* The salt exists for unpredictability, not uniqueness: the per-owner nonce already guarantees
|
|
25
25
|
* distinct keys. Random bits mean only the creator can predict the key of an entity they are about
|
package/src/entity/operations.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Address, encodeAbiParameters, type Hex, parseAbi, parseAbiParameters } from "viem"
|
|
2
|
-
import type { AbiAttribute } from "../
|
|
2
|
+
import type { AbiAttribute } from "../schema/codec"
|
|
3
3
|
import type { EncodedExpiry } from "./expiry"
|
|
4
4
|
import type { predictEntityKey } from "./key"
|
|
5
5
|
import { OperationType } from "./params"
|
package/src/errors.ts
CHANGED
|
@@ -37,19 +37,10 @@ export class EmptyPatchError extends Error {
|
|
|
37
37
|
|
|
38
38
|
constructor(entityKey: string) {
|
|
39
39
|
super(
|
|
40
|
-
`The patch for entity ${entityKey} has nothing to apply. Pass "set" to write
|
|
41
|
-
'"unset" to remove
|
|
40
|
+
`The patch for entity ${entityKey} has nothing to apply. Pass "set" to write columns, or ` +
|
|
41
|
+
'"unset" to remove optional ones.',
|
|
42
42
|
)
|
|
43
43
|
this.name = "EmptyPatchError"
|
|
44
44
|
this.entityKey = entityKey
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
-
|
|
48
|
-
export class InvalidContentTypeError extends Error {
|
|
49
|
-
constructor(contentType: string) {
|
|
50
|
-
super(
|
|
51
|
-
`Invalid content type "${contentType}". Must follow RFC 2045 MIME grammar and be lowercase only (e.g. "text/plain").`,
|
|
52
|
-
)
|
|
53
|
-
this.name = "InvalidContentTypeError"
|
|
54
|
-
}
|
|
55
|
-
}
|
package/src/index.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
* @module main
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
// the attribute type system
|
|
6
|
-
export * from "./attr"
|
|
7
5
|
// export main arkiv stuff
|
|
8
6
|
export type { ArkivClient } from "./clients/baseClient"
|
|
9
7
|
export type { PublicArkivClient } from "./clients/createPublicClient"
|
|
@@ -19,18 +17,32 @@ export * from "./errors"
|
|
|
19
17
|
// types referenced by the public client, must be reachable from the package root
|
|
20
18
|
// see #106
|
|
21
19
|
export type {
|
|
22
|
-
|
|
20
|
+
AttributeRef,
|
|
21
|
+
DynamicSelection,
|
|
23
22
|
Expression,
|
|
23
|
+
FromOptions,
|
|
24
24
|
FullEntity,
|
|
25
|
-
|
|
25
|
+
FullRow,
|
|
26
|
+
KnownSystemFields,
|
|
27
|
+
NonEmptySelection,
|
|
28
|
+
PartialRow,
|
|
29
|
+
QueryBuilderBase,
|
|
26
30
|
QueryResult,
|
|
31
|
+
ResultOf,
|
|
32
|
+
Row,
|
|
33
|
+
SchemaRef,
|
|
27
34
|
SelectArg,
|
|
35
|
+
Selected,
|
|
36
|
+
Selection,
|
|
37
|
+
SelectionOf,
|
|
38
|
+
SystemField,
|
|
28
39
|
} from "./query"
|
|
29
|
-
export { SelectQueryBuilder } from "./query"
|
|
40
|
+
export { CountQueryBuilder, SelectQueryBuilder, SYSTEM_FIELDS } from "./query"
|
|
41
|
+
// schemas and the attribute type system
|
|
42
|
+
export * from "./schema"
|
|
30
43
|
// re-export arkiv types in main index file
|
|
31
44
|
export * from "./types"
|
|
32
45
|
// re-export chosen utils
|
|
33
46
|
export { chainFromName } from "./utils/chains"
|
|
34
47
|
// every create needs an expiry, and this is the only way to build one
|
|
35
48
|
export { type DeadlineOptions, ExpirationTime } from "./utils/expirationTime"
|
|
36
|
-
export { jsonToPayload, stringToPayload } from "./utils/payload"
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import type { Hex } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../clients/baseClient"
|
|
3
|
+
import { addr, type ColumnsOf, SCHEMA_ATTRIBUTE, type Schema } from "../schema"
|
|
4
|
+
import { InvalidPredicateError } from "./errors"
|
|
5
|
+
import {
|
|
6
|
+
and,
|
|
7
|
+
checkAgainstSchema,
|
|
8
|
+
type Expression,
|
|
9
|
+
eq,
|
|
10
|
+
type RefsOf,
|
|
11
|
+
type SchemaRef,
|
|
12
|
+
} from "./expression"
|
|
13
|
+
|
|
14
|
+
/** The expressions in a `where` call — passed one by one, or as one array. */
|
|
15
|
+
type WhereExpressions<E extends readonly (Expression | readonly Expression[])[]> =
|
|
16
|
+
E[number] extends infer X ? (X extends readonly Expression[] ? X[number] : X) : never
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Human-readable messages for compile-time errors when a filter does not fit the schema.
|
|
20
|
+
*/
|
|
21
|
+
type FilterProblem<R, S extends Schema> = R extends {
|
|
22
|
+
readonly name: infer N extends string
|
|
23
|
+
readonly type: infer T extends string
|
|
24
|
+
}
|
|
25
|
+
? R extends SchemaRef<S>
|
|
26
|
+
? never
|
|
27
|
+
: N extends SchemaRef<S>["name"]
|
|
28
|
+
? `"${N}" has type ${Extract<SchemaRef<S>, { name: N }>["type"]}, but is compared against a value of type ${T}`
|
|
29
|
+
: N extends keyof ColumnsOf<S>
|
|
30
|
+
? ColumnsOf<S>[N]["type"] extends "bytes"
|
|
31
|
+
? `"${N}" is a bytes column, and bytes are never indexed, so it cannot be filtered on. Index a str, bytes32 or key derived from it instead`
|
|
32
|
+
: `"${N}" is not indexed, so it cannot be filtered on. Declare it ${ColumnsOf<S>[N]["type"]}().indexed() to filter on it`
|
|
33
|
+
: `"${N}" is not a column of this schema`
|
|
34
|
+
: never
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* `where`'s check that every filter fits the schema — `unknown` when it does, or when there is no
|
|
38
|
+
* schema, and otherwise an object whose one property is named after the problem. No expression has
|
|
39
|
+
* that property, so the compiler reports the sentence as the property it is missing.
|
|
40
|
+
*/
|
|
41
|
+
type FilterCheck<
|
|
42
|
+
E extends readonly (Expression | readonly Expression[])[],
|
|
43
|
+
TSchema,
|
|
44
|
+
> = TSchema extends Schema
|
|
45
|
+
? [FilterProblem<RefsOf<WhereExpressions<E>>, TSchema>] extends [infer P extends string]
|
|
46
|
+
? [P] extends [never]
|
|
47
|
+
? unknown
|
|
48
|
+
: { [K in P]: never }
|
|
49
|
+
: unknown
|
|
50
|
+
: unknown
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* What every query builder shares: the client, the filters — `where`, `ownedBy`, `createdBy` and a
|
|
54
|
+
* named schema's tag — and the block to read at. `client.select()` and `client.count()` build on it,
|
|
55
|
+
* so the same filters always send the same query string, whichever of them runs it.
|
|
56
|
+
*
|
|
57
|
+
* Not constructed directly: create a builder with `client.select(...)` or `client.count()`.
|
|
58
|
+
*
|
|
59
|
+
* @typeParam TSchema - The schema named with `from()`, if any, which `where` is checked against.
|
|
60
|
+
*/
|
|
61
|
+
export abstract class QueryBuilderBase<TSchema extends Schema | undefined = undefined> {
|
|
62
|
+
protected readonly _client: ArkivClient
|
|
63
|
+
protected _schema: Schema | undefined
|
|
64
|
+
protected readonly _filters: Expression[] = []
|
|
65
|
+
protected _ownedBy: Expression | undefined
|
|
66
|
+
protected _createdBy: Expression | undefined
|
|
67
|
+
protected _atBlock: bigint | undefined
|
|
68
|
+
|
|
69
|
+
constructor(client: ArkivClient) {
|
|
70
|
+
this._client = client
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* This builder's state in a new builder of the same class, re-typed by the caller. Every field is
|
|
75
|
+
* carried over, so one a subclass adds is not dropped by `from()`; the filter list, which is
|
|
76
|
+
* mutated in place, is copied rather than shared.
|
|
77
|
+
*
|
|
78
|
+
* A copy, not `this` re-typed: a builder typed for one schema must never be the same object as
|
|
79
|
+
* one typed for another, or `base.from(Posts)` and `base.from(Users)` would share their state.
|
|
80
|
+
*/
|
|
81
|
+
protected copy<B extends QueryBuilderBase<Schema | undefined>>(): B {
|
|
82
|
+
const copy = Object.create(Object.getPrototypeOf(this)) as B
|
|
83
|
+
return Object.assign(copy, this, { _filters: [...this._filters] })
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Adds filters. Everything passed here, and across repeated calls, must hold.
|
|
88
|
+
*
|
|
89
|
+
* After `from()`, every attribute a filter names must be an indexed column of the schema,
|
|
90
|
+
* compared against a value of its type — anything else is a compile error.
|
|
91
|
+
*
|
|
92
|
+
* @param expressions - The expressions, as separate arguments or one array.
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* builder.where(eq("category", "docs"))
|
|
96
|
+
* builder.where(gte("level", i32(10)), lt("level", i32(20)))
|
|
97
|
+
* builder.where(or(eq("status", "open"), not(exists("closedAt"))))
|
|
98
|
+
*/
|
|
99
|
+
where<const E extends readonly (Expression | readonly Expression[])[]>(
|
|
100
|
+
...expressions: E & FilterCheck<E, TSchema>
|
|
101
|
+
): this {
|
|
102
|
+
this._filters.push(...expressions.flat())
|
|
103
|
+
return this
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Restricts the query to entities this account owns — shorthand for
|
|
108
|
+
* `where(eq("$owner", addr(owner)))`. Calling it again replaces the filter.
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* builder.ownedBy("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
|
|
112
|
+
*/
|
|
113
|
+
ownedBy(owner: Hex): this {
|
|
114
|
+
this._ownedBy = eq("$owner", addr(owner))
|
|
115
|
+
return this
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Restricts the query to entities this account created — shorthand for
|
|
120
|
+
* `where(eq("$creator", addr(creator)))`. Calling it again replaces the filter.
|
|
121
|
+
*
|
|
122
|
+
* The creator never changes; the owner does, so these differ after a transfer.
|
|
123
|
+
*/
|
|
124
|
+
createdBy(creator: Hex): this {
|
|
125
|
+
this._createdBy = eq("$creator", addr(creator))
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Reads the state as of a given block rather than the head. The block must be within the range
|
|
131
|
+
* the node retains.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* builder.atBlock(1_297_000n)
|
|
135
|
+
*/
|
|
136
|
+
atBlock(block: bigint): this {
|
|
137
|
+
this._atBlock = block
|
|
138
|
+
return this
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* The query string this builder will send — the same text the node parses: its filters,
|
|
143
|
+
* `ownedBy` and `createdBy`, and a named schema's tag, ANDed.
|
|
144
|
+
*
|
|
145
|
+
* @throws {InvalidPredicateError} If no filter has been added, or — after `from()` — a filter
|
|
146
|
+
* does not fit the schema. A named schema's own tag filter counts as a filter.
|
|
147
|
+
*/
|
|
148
|
+
toString(): string {
|
|
149
|
+
const filters = [...this._filters]
|
|
150
|
+
if (this._ownedBy) filters.push(this._ownedBy)
|
|
151
|
+
if (this._createdBy) filters.push(this._createdBy)
|
|
152
|
+
const schema = this._schema
|
|
153
|
+
if (schema !== undefined) {
|
|
154
|
+
for (const filter of filters) checkAgainstSchema(filter, schema)
|
|
155
|
+
// Added after the check: the tag is not a column, and the caller never wrote this filter.
|
|
156
|
+
if (schema.tag !== undefined) filters.unshift(eq(SCHEMA_ATTRIBUTE, schema.tag))
|
|
157
|
+
}
|
|
158
|
+
if (filters.length === 0) {
|
|
159
|
+
throw new InvalidPredicateError(
|
|
160
|
+
"A query needs at least one filter — the language has no spelling for 'match every " +
|
|
161
|
+
"entity'. Add a where(...) predicate, or narrow by ownedBy(...) / createdBy(...).",
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
return and(filters).toString()
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "bun:test"
|
|
2
|
+
import type { ArkivClient } from "../clients/baseClient"
|
|
3
|
+
import { dec, defineSchema, str } from "../schema"
|
|
4
|
+
import { CountQueryBuilder } from "./countBuilder"
|
|
5
|
+
import { InvalidPredicateError } from "./errors"
|
|
6
|
+
import { eq, gt } from "./expression"
|
|
7
|
+
|
|
8
|
+
const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as const
|
|
9
|
+
const Posts = defineSchema("my-app.posts", { category: str().indexed(), score: dec().indexed() })
|
|
10
|
+
|
|
11
|
+
/** A client whose `arkiv_query` answers with `result`. */
|
|
12
|
+
function makeClient(result: unknown = { data: [], blockNumber: "0x5", count: "0x2a" }) {
|
|
13
|
+
const request = vi.fn(async () => result)
|
|
14
|
+
return { client: { request } as unknown as ArkivClient, request }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** The `[queryString, options]` of the first `arkiv_query` call. */
|
|
18
|
+
function sent(request: ReturnType<typeof makeClient>["request"]) {
|
|
19
|
+
const [[call]] = request.mock.calls as unknown as [
|
|
20
|
+
[{ method: string; params: [string, Record<string, unknown>] }],
|
|
21
|
+
]
|
|
22
|
+
return { method: call.method, query: call.params[0], options: call.params[1] }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("CountQueryBuilder", () => {
|
|
26
|
+
it("asks for the count alone and resolves to it", async () => {
|
|
27
|
+
const { client, request } = makeClient()
|
|
28
|
+
const total = await new CountQueryBuilder(client).where(eq("category", "news")).fetch()
|
|
29
|
+
|
|
30
|
+
expect(total).toBe(42n)
|
|
31
|
+
expect(sent(request).method).toBe("arkiv_query")
|
|
32
|
+
expect(sent(request).query).toBe("category = str('news')")
|
|
33
|
+
// Nothing but the count is selected, so the node sends no rows and no cursor.
|
|
34
|
+
expect(sent(request).options).toEqual({ select: { count: true } })
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("filters as select() does: schema tag, where, owner, creator and block", async () => {
|
|
38
|
+
const { client, request } = makeClient()
|
|
39
|
+
await new CountQueryBuilder(client)
|
|
40
|
+
.from(Posts)
|
|
41
|
+
.where(gt("score", dec("4")))
|
|
42
|
+
.ownedBy(ACCOUNT)
|
|
43
|
+
.createdBy(ACCOUNT)
|
|
44
|
+
.atBlock(5n)
|
|
45
|
+
.fetch()
|
|
46
|
+
|
|
47
|
+
expect(sent(request).query).toBe(
|
|
48
|
+
"arkiv.schema = str('my-app.posts') AND score > dec(4) AND " +
|
|
49
|
+
`$owner = addr(${ACCOUNT}) AND $creator = addr(${ACCOUNT})`,
|
|
50
|
+
)
|
|
51
|
+
expect(sent(request).options).toEqual({ select: { count: true }, atBlock: "0x5" })
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it("needs no other filter through a named schema", async () => {
|
|
55
|
+
const { client, request } = makeClient()
|
|
56
|
+
await new CountQueryBuilder(client).from(Posts).fetch()
|
|
57
|
+
expect(sent(request).query).toBe("arkiv.schema = str('my-app.posts')")
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
it("checks filters against the schema, at compile time and when it runs", async () => {
|
|
61
|
+
const { client } = makeClient()
|
|
62
|
+
// @ts-expect-error - score is a dec column
|
|
63
|
+
const wrongType = new CountQueryBuilder(client).from(Posts).where(eq("score", 5))
|
|
64
|
+
await expect(wrongType.fetch()).rejects.toThrow(InvalidPredicateError)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it("rejects a count with no filter at all", async () => {
|
|
68
|
+
const { client, request } = makeClient()
|
|
69
|
+
await expect(new CountQueryBuilder(client).fetch()).rejects.toThrow(InvalidPredicateError)
|
|
70
|
+
expect(request).not.toHaveBeenCalled()
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
it("fails rather than guessing when the node sends no count", async () => {
|
|
74
|
+
const { client } = makeClient({ data: [], blockNumber: "0x1" })
|
|
75
|
+
await expect(new CountQueryBuilder(client).where(eq("a", 1)).fetch()).rejects.toThrow(
|
|
76
|
+
/returned no count/,
|
|
77
|
+
)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("leaves the builder from() was called on without the schema", async () => {
|
|
81
|
+
const { client, request } = makeClient()
|
|
82
|
+
const base = new CountQueryBuilder(client).where(eq("category", "news"))
|
|
83
|
+
await base.from(Posts).fetch()
|
|
84
|
+
await base.fetch()
|
|
85
|
+
const [, second] = request.mock.calls as unknown as [
|
|
86
|
+
unknown,
|
|
87
|
+
[{ params: [string, Record<string, unknown>] }],
|
|
88
|
+
]
|
|
89
|
+
expect(second[0].params[0]).toBe("category = str('news')")
|
|
90
|
+
})
|
|
91
|
+
})
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { isSchema, type Schema, SchemaDefinitionError } from "../schema"
|
|
2
|
+
import { QueryBuilderBase } from "./builderBase"
|
|
3
|
+
import { runQuery } from "./engine"
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Builds and runs a count: how many entities a query matches, without fetching any of them.
|
|
7
|
+
*
|
|
8
|
+
* It filters exactly as `client.select()` does — {@link from}, `where`, `ownedBy`, `createdBy` and
|
|
9
|
+
* `atBlock` are the same methods, from the same base — but has nothing to select and no pages, so
|
|
10
|
+
* {@link fetch} resolves to a `bigint`. Create one with `client.count()`.
|
|
11
|
+
*
|
|
12
|
+
* To show the total next to a page of rows, use `select(...).withCount()` instead, which counts in
|
|
13
|
+
* the same request.
|
|
14
|
+
*
|
|
15
|
+
* @typeParam TSchema - The schema named with {@link from}, if any.
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* const total = await client.count().from(Posts).ownedBy(account.address).fetch()
|
|
19
|
+
*/
|
|
20
|
+
export class CountQueryBuilder<
|
|
21
|
+
TSchema extends Schema | undefined = undefined,
|
|
22
|
+
> extends QueryBuilderBase<TSchema> {
|
|
23
|
+
/**
|
|
24
|
+
* Counts through a schema: `where` is checked against its indexed columns, and a named schema
|
|
25
|
+
* counts only entities tagged with its name — so it needs no other filter.
|
|
26
|
+
*
|
|
27
|
+
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
|
|
28
|
+
*
|
|
29
|
+
* @throws {SchemaDefinitionError} If `schema` was not built with `defineSchema`.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* client.count().from(Posts) // every post
|
|
33
|
+
*/
|
|
34
|
+
from<S extends Schema>(schema: S): CountQueryBuilder<S> {
|
|
35
|
+
if (!isSchema(schema)) {
|
|
36
|
+
throw new SchemaDefinitionError("from() takes a schema built with defineSchema(...)")
|
|
37
|
+
}
|
|
38
|
+
const typed = this.copy<CountQueryBuilder<S>>()
|
|
39
|
+
typed._schema = schema
|
|
40
|
+
return typed
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Runs the count.
|
|
45
|
+
*
|
|
46
|
+
* Counts every entity the filters match at the block, whether or not it fits the schema — what
|
|
47
|
+
* `select().from(schema)` leaves out of its pages is still counted.
|
|
48
|
+
*
|
|
49
|
+
* @throws {InvalidPredicateError} If no filter has been added, or a filter does not fit the
|
|
50
|
+
* schema.
|
|
51
|
+
* @throws {QueryError} If the node rejects the query.
|
|
52
|
+
* @throws If the node returns no count — it predates the count projection.
|
|
53
|
+
*/
|
|
54
|
+
async fetch(): Promise<bigint> {
|
|
55
|
+
// Nothing but the count is selected, so the node returns no rows and no cursor.
|
|
56
|
+
const response = await runQuery(this._client, {
|
|
57
|
+
query: this.toString(),
|
|
58
|
+
select: {},
|
|
59
|
+
atBlock: this._atBlock,
|
|
60
|
+
count: true,
|
|
61
|
+
})
|
|
62
|
+
if (response.count === undefined) {
|
|
63
|
+
throw new Error("The node returned no count. It may predate the count projection.")
|
|
64
|
+
}
|
|
65
|
+
return response.count
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/query/engine.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { asQueryError } from "./errors"
|
|
|
8
8
|
|
|
9
9
|
const logger = getLogger("query:engine")
|
|
10
10
|
|
|
11
|
-
export const MAX_LIMIT =
|
|
11
|
+
export const MAX_LIMIT = 100
|
|
12
12
|
|
|
13
13
|
/** One `arkiv_query` call. */
|
|
14
14
|
export type QueryRequest = {
|
|
@@ -22,6 +22,8 @@ export type QueryRequest = {
|
|
|
22
22
|
cursor?: string | undefined
|
|
23
23
|
/** Block height to read at. Defaults to the head. */
|
|
24
24
|
atBlock?: bigint | undefined
|
|
25
|
+
/** Whether to also count every entity the query matches. */
|
|
26
|
+
count?: boolean | undefined
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/** One page of results. */
|
|
@@ -31,15 +33,18 @@ export type QueryResponse = {
|
|
|
31
33
|
blockNumber: bigint
|
|
32
34
|
/** The cursor for the next page, or `undefined` when this was the last one. */
|
|
33
35
|
cursor: string | undefined
|
|
36
|
+
/** How many entities the query matches, if the request asked to count them. */
|
|
37
|
+
count: bigint | undefined
|
|
34
38
|
}
|
|
35
39
|
|
|
36
40
|
/**
|
|
37
41
|
* Runs one query and decodes the page it returns.
|
|
38
42
|
*
|
|
43
|
+
* @throws If `limit` is not an integer from 1 to {@link MAX_LIMIT}.
|
|
39
44
|
* @throws {QueryError} If the node rejects the query.
|
|
40
45
|
*/
|
|
41
46
|
export async function runQuery(client: ArkivClient, request: QueryRequest): Promise<QueryResponse> {
|
|
42
|
-
const { query, select, limit, cursor, atBlock } = request
|
|
47
|
+
const { query, select, limit, cursor, atBlock, count } = request
|
|
43
48
|
|
|
44
49
|
if (limit !== undefined) {
|
|
45
50
|
if (!Number.isInteger(limit) || limit < 1) {
|
|
@@ -54,7 +59,7 @@ export async function runQuery(client: ArkivClient, request: QueryRequest): Prom
|
|
|
54
59
|
}
|
|
55
60
|
|
|
56
61
|
const queryOptions: RpcQueryOptions = {
|
|
57
|
-
select,
|
|
62
|
+
select: count ? { ...select, count: true } : select,
|
|
58
63
|
...(atBlock !== undefined && { atBlock: numberToHex(atBlock) }),
|
|
59
64
|
...(limit !== undefined && { limit: numberToHex(limit) }),
|
|
60
65
|
...(cursor !== undefined && { cursor }),
|
|
@@ -74,5 +79,6 @@ export async function runQuery(client: ArkivClient, request: QueryRequest): Prom
|
|
|
74
79
|
entities: result.data.map((rpcEntity) => entityFromRpcResult(rpcEntity)),
|
|
75
80
|
blockNumber: BigInt(result.blockNumber),
|
|
76
81
|
cursor: result.cursor,
|
|
82
|
+
count: result.count === undefined ? undefined : BigInt(result.count),
|
|
77
83
|
}
|
|
78
84
|
}
|