@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,19 +1,32 @@
|
|
|
1
1
|
import { describe, expect, it, vi } from "bun:test"
|
|
2
2
|
import type { Hex } from "viem"
|
|
3
|
-
import { type Attributes, i32 } from "../attr"
|
|
4
3
|
import type { ArkivClient } from "../clients/baseClient"
|
|
5
4
|
import { NoMoreResultsError } from "../errors"
|
|
5
|
+
import {
|
|
6
|
+
type AddrValue,
|
|
7
|
+
type ArkivValue,
|
|
8
|
+
type Attributes,
|
|
9
|
+
addr,
|
|
10
|
+
type BytesValue,
|
|
11
|
+
bytes,
|
|
12
|
+
dec,
|
|
13
|
+
defineSchema,
|
|
14
|
+
i32,
|
|
15
|
+
SchemaMismatchError,
|
|
16
|
+
type StrValue,
|
|
17
|
+
str,
|
|
18
|
+
} from "../schema"
|
|
6
19
|
import type { RpcEntity } from "../types/rpcSchema"
|
|
7
20
|
import { MAX_LIMIT } from "./engine"
|
|
8
21
|
import { InvalidPredicateError, QueryError } from "./errors"
|
|
9
|
-
import { eq, gte, or } from "./expression"
|
|
22
|
+
import { eq, exists, gte, or } from "./expression"
|
|
10
23
|
import { SelectQueryBuilder } from "./queryBuilder"
|
|
11
|
-
import type {
|
|
24
|
+
import type { DynamicSelection, SelectArg, Selection } from "./selection"
|
|
12
25
|
|
|
13
|
-
const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
|
|
26
|
+
const ACCOUNT = "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045" as const
|
|
14
27
|
const KEY = `0x${"ab".repeat(32)}` as const
|
|
15
28
|
|
|
16
|
-
type Page = { data: RpcEntity[]; blockNumber: string; cursor?: string }
|
|
29
|
+
type Page = { data: RpcEntity[]; blockNumber: string; cursor?: string; count?: string }
|
|
17
30
|
|
|
18
31
|
/** A client whose `arkiv_query` serves the given pages in order. */
|
|
19
32
|
function makeClient(...pages: Page[]) {
|
|
@@ -35,7 +48,7 @@ function sent(request: ReturnType<typeof makeClient>["request"], call = 0) {
|
|
|
35
48
|
describe("the query it sends", () => {
|
|
36
49
|
it("ANDs everything across repeated where() calls", async () => {
|
|
37
50
|
const { client, request } = makeClient()
|
|
38
|
-
await new SelectQueryBuilder(client, { key: true })
|
|
51
|
+
await new SelectQueryBuilder(client, { $key: true })
|
|
39
52
|
.where(gte("level", i32(10)))
|
|
40
53
|
.where(eq("category", "docs"), or(eq("status", "open"), eq("status", "review")))
|
|
41
54
|
.fetch()
|
|
@@ -49,13 +62,13 @@ describe("the query it sends", () => {
|
|
|
49
62
|
|
|
50
63
|
it("takes an array of expressions", async () => {
|
|
51
64
|
const { client, request } = makeClient()
|
|
52
|
-
await new SelectQueryBuilder(client, { key: true }).where([eq("a", 1), eq("b", 2)]).fetch()
|
|
65
|
+
await new SelectQueryBuilder(client, { $key: true }).where([eq("a", 1), eq("b", 2)]).fetch()
|
|
53
66
|
expect(sent(request).query).toBe("a = i32(1) AND b = i32(2)")
|
|
54
67
|
})
|
|
55
68
|
|
|
56
69
|
it("turns ownedBy and createdBy into typed system predicates", async () => {
|
|
57
70
|
const { client, request } = makeClient()
|
|
58
|
-
await new SelectQueryBuilder(client, { key: true })
|
|
71
|
+
await new SelectQueryBuilder(client, { $key: true })
|
|
59
72
|
.where(eq("category", "docs"))
|
|
60
73
|
.ownedBy(ACCOUNT)
|
|
61
74
|
.createdBy(ACCOUNT)
|
|
@@ -69,20 +82,20 @@ describe("the query it sends", () => {
|
|
|
69
82
|
it("replaces rather than ANDs a second ownedBy — two owners would match nothing", async () => {
|
|
70
83
|
const other = "0x1111111111111111111111111111111111111111"
|
|
71
84
|
const { client, request } = makeClient()
|
|
72
|
-
await new SelectQueryBuilder(client, { key: true }).ownedBy(ACCOUNT).ownedBy(other).fetch()
|
|
85
|
+
await new SelectQueryBuilder(client, { $key: true }).ownedBy(ACCOUNT).ownedBy(other).fetch()
|
|
73
86
|
expect(sent(request).query).toBe(`$owner = addr(${other})`)
|
|
74
87
|
})
|
|
75
88
|
|
|
76
89
|
it("is readable without running anything", () => {
|
|
77
90
|
const { client } = makeClient()
|
|
78
|
-
const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
|
|
91
|
+
const builder = new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1))
|
|
79
92
|
expect(builder.toString()).toBe("a = i32(1)")
|
|
80
93
|
expect(`${builder}`).toBe("a = i32(1)")
|
|
81
94
|
})
|
|
82
95
|
|
|
83
96
|
it("refuses a query with no filter, which the language cannot spell", async () => {
|
|
84
97
|
const { client, request } = makeClient()
|
|
85
|
-
const builder = new SelectQueryBuilder(client, { key: true })
|
|
98
|
+
const builder = new SelectQueryBuilder(client, { $key: true })
|
|
86
99
|
expect(() => builder.toString()).toThrow(InvalidPredicateError)
|
|
87
100
|
await expect(builder.fetch()).rejects.toThrow(/at least one filter/)
|
|
88
101
|
expect(request).not.toHaveBeenCalled()
|
|
@@ -92,7 +105,9 @@ describe("the query it sends", () => {
|
|
|
92
105
|
describe("the options it sends", () => {
|
|
93
106
|
it("sends the resolved selection, and nothing else by default", async () => {
|
|
94
107
|
const { client, request } = makeClient()
|
|
95
|
-
await new SelectQueryBuilder(client, { key: true, attributes: true })
|
|
108
|
+
await new SelectQueryBuilder(client, { $key: true, $attributes: true })
|
|
109
|
+
.where(eq("a", 1))
|
|
110
|
+
.fetch()
|
|
96
111
|
|
|
97
112
|
const { options } = sent(request)
|
|
98
113
|
expect(options).toEqual({ select: expect.objectContaining({ key: true, attributes: true }) })
|
|
@@ -102,7 +117,7 @@ describe("the options it sends", () => {
|
|
|
102
117
|
|
|
103
118
|
it("sends limit, cursor and atBlock as the RPC spells them", async () => {
|
|
104
119
|
const { client, request } = makeClient()
|
|
105
|
-
await new SelectQueryBuilder(client, { key: true })
|
|
120
|
+
await new SelectQueryBuilder(client, { $key: true })
|
|
106
121
|
.where(eq("a", 1))
|
|
107
122
|
.limit(100)
|
|
108
123
|
.cursor("b64:abc")
|
|
@@ -119,7 +134,7 @@ describe("the options it sends", () => {
|
|
|
119
134
|
|
|
120
135
|
it("rejects a page size the node would reject", async () => {
|
|
121
136
|
const { client, request } = makeClient()
|
|
122
|
-
const query = () => new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
|
|
137
|
+
const query = () => new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1))
|
|
123
138
|
await expect(
|
|
124
139
|
query()
|
|
125
140
|
.limit(MAX_LIMIT + 1)
|
|
@@ -132,6 +147,55 @@ describe("the options it sends", () => {
|
|
|
132
147
|
})
|
|
133
148
|
})
|
|
134
149
|
|
|
150
|
+
describe("withCount", () => {
|
|
151
|
+
it("carries the total on every page, typed bigint", async () => {
|
|
152
|
+
const { client, request } = makeClient(
|
|
153
|
+
{ data: [{ key: KEY }], blockNumber: "0x1", cursor: "b64:next", count: "0x2" },
|
|
154
|
+
{ data: [{ key: KEY }], blockNumber: "0x1", count: "0x2" },
|
|
155
|
+
)
|
|
156
|
+
const first = await new SelectQueryBuilder(client, { $key: true })
|
|
157
|
+
.where(eq("a", 1))
|
|
158
|
+
.withCount()
|
|
159
|
+
.limit(1)
|
|
160
|
+
.fetch()
|
|
161
|
+
const second = await first.next()
|
|
162
|
+
|
|
163
|
+
const total: bigint = first.count
|
|
164
|
+
const again: bigint = second.count
|
|
165
|
+
expect(total).toBe(2n)
|
|
166
|
+
expect(again).toBe(2n)
|
|
167
|
+
expect(sent(request, 0).options.select).toMatchObject({ key: true, count: true })
|
|
168
|
+
expect(sent(request, 1).options.select).toMatchObject({ key: true, count: true })
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it("fails rather than typing a missing count as bigint", async () => {
|
|
172
|
+
const { client } = makeClient({ data: [], blockNumber: "0x1" })
|
|
173
|
+
await expect(
|
|
174
|
+
new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1)).withCount().fetch(),
|
|
175
|
+
).rejects.toThrow(/returned no count/)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("leaves count undefined, in the type too, unless asked for", async () => {
|
|
179
|
+
const { client, request } = makeClient({ data: [], blockNumber: "0x1" })
|
|
180
|
+
const page = await new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1)).fetch()
|
|
181
|
+
const nothing: undefined = page.count
|
|
182
|
+
expect(nothing).toBeUndefined()
|
|
183
|
+
expect(sent(request).options.select).not.toHaveProperty("count")
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it("survives from(), and leaves the builder it was called on uncounted", async () => {
|
|
187
|
+
const Posts = defineSchema("posts", { category: str().indexed() })
|
|
188
|
+
const { client, request } = makeClient({ data: [], blockNumber: "0x1", count: "0x0" })
|
|
189
|
+
const base = new SelectQueryBuilder(client, { $key: true }).where(eq("category", "news"))
|
|
190
|
+
const counted = base.withCount().from(Posts)
|
|
191
|
+
const page = await counted.fetch()
|
|
192
|
+
const total: bigint = page.count
|
|
193
|
+
expect(total).toBe(0n)
|
|
194
|
+
await base.fetch()
|
|
195
|
+
expect(sent(request, 1).options.select).not.toHaveProperty("count")
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
135
199
|
describe("decoding", () => {
|
|
136
200
|
it("builds entities from exactly the fields the row carries", async () => {
|
|
137
201
|
const { client } = makeClient({
|
|
@@ -142,42 +206,70 @@ describe("decoding", () => {
|
|
|
142
206
|
owner: ACCOUNT,
|
|
143
207
|
expiresAt: "0x92e21",
|
|
144
208
|
creationFlags: { readonly: false, permissionlessExtension: true, raw: 2 },
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
209
|
+
attributeSchema: [
|
|
210
|
+
{ name: "version", type: "i32", indexed: true },
|
|
211
|
+
{ name: "body", type: "bytes", indexed: false },
|
|
212
|
+
],
|
|
148
213
|
attributes: [
|
|
149
214
|
{ name: "projectId", type: "str", value: "alice-123" },
|
|
150
215
|
{ name: "version", type: "i32", value: 4 },
|
|
151
216
|
{ name: "balance", type: "u256", value: "0xf4240" },
|
|
152
217
|
{ name: "score", type: "dec", value: "3.5" },
|
|
218
|
+
{ name: "body", type: "bytes", value: "0x7b7d" },
|
|
153
219
|
],
|
|
154
220
|
},
|
|
155
221
|
],
|
|
156
222
|
})
|
|
157
223
|
|
|
158
|
-
const page = await new SelectQueryBuilder(client,
|
|
224
|
+
const page = await new SelectQueryBuilder(client, {
|
|
225
|
+
$key: true,
|
|
226
|
+
$expiresAt: true,
|
|
227
|
+
$creationFlags: true,
|
|
228
|
+
$creator: true,
|
|
229
|
+
$createdAt: true,
|
|
230
|
+
$attributes: true,
|
|
231
|
+
$attributeSchema: true,
|
|
232
|
+
})
|
|
233
|
+
.where(eq("a", 1))
|
|
234
|
+
.fetch()
|
|
159
235
|
const entity = page.entities[0]
|
|
160
236
|
|
|
161
|
-
expect(entity
|
|
162
|
-
expect(entity
|
|
163
|
-
expect(entity
|
|
237
|
+
expect(entity.$key).toBe(KEY)
|
|
238
|
+
expect(entity.$expiresAt).toBe(0x92e21n)
|
|
239
|
+
expect(entity.$creationFlags).toEqual({
|
|
164
240
|
readonly: false,
|
|
165
241
|
permissionlessExtension: true,
|
|
166
242
|
raw: 2,
|
|
167
243
|
})
|
|
168
|
-
expect(entity
|
|
244
|
+
expect(entity.$attributes).toEqual({
|
|
169
245
|
projectId: { type: "str", value: "alice-123" },
|
|
170
246
|
version: { type: "i32", value: 4 },
|
|
171
247
|
balance: { type: "u256", value: 1_000_000n },
|
|
172
248
|
score: { type: "dec", value: "3.5" },
|
|
249
|
+
body: { type: "bytes", value: "0x7b7d" },
|
|
250
|
+
})
|
|
251
|
+
expect(entity.$attributeSchema).toEqual({
|
|
252
|
+
version: { type: "i32", indexed: true },
|
|
253
|
+
body: { type: "bytes", indexed: false },
|
|
173
254
|
})
|
|
174
|
-
expect(entity.attributeSchema).toEqual({ version: "i32" })
|
|
175
|
-
expect(entity.toJson()).toEqual({})
|
|
176
255
|
// Unselected stays undefined rather than becoming an empty value.
|
|
177
|
-
expect(entity
|
|
178
|
-
expect(entity
|
|
256
|
+
expect(entity.$creator).toBeUndefined()
|
|
257
|
+
expect(entity.$createdAt).toBeUndefined()
|
|
179
258
|
expect(page.blockNumber).toBe(0x8e1ffn)
|
|
180
259
|
})
|
|
260
|
+
|
|
261
|
+
it("leaves indexed undefined when the node does not say, rather than guessing", async () => {
|
|
262
|
+
const { client } = makeClient({
|
|
263
|
+
blockNumber: "0x1",
|
|
264
|
+
data: [{ key: KEY, attributeSchema: [{ name: "version", type: "i32" }] }],
|
|
265
|
+
})
|
|
266
|
+
const page = await new SelectQueryBuilder(client, { $attributeSchema: true })
|
|
267
|
+
.where(eq("a", 1))
|
|
268
|
+
.fetch()
|
|
269
|
+
expect(page.entities[0].$attributeSchema).toEqual({
|
|
270
|
+
version: { type: "i32", indexed: undefined },
|
|
271
|
+
})
|
|
272
|
+
})
|
|
181
273
|
})
|
|
182
274
|
|
|
183
275
|
describe("pagination", () => {
|
|
@@ -193,7 +285,7 @@ describe("pagination", () => {
|
|
|
193
285
|
page("b64:2", "0x3"),
|
|
194
286
|
page(undefined, "0x4"),
|
|
195
287
|
)
|
|
196
|
-
const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).limit(2)
|
|
288
|
+
const builder = new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1)).limit(2)
|
|
197
289
|
|
|
198
290
|
const first = await builder.fetch()
|
|
199
291
|
expect(first.entities).toHaveLength(2)
|
|
@@ -210,7 +302,7 @@ describe("pagination", () => {
|
|
|
210
302
|
|
|
211
303
|
it("leaves an already-read page readable", async () => {
|
|
212
304
|
const { client } = makeClient(page("b64:1", "0x1"), page(undefined, "0x2"))
|
|
213
|
-
const first = await new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
|
|
305
|
+
const first = await new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1)).fetch()
|
|
214
306
|
await first.next()
|
|
215
307
|
// next() returns the following page rather than mutating this one.
|
|
216
308
|
expect(first.entities).toHaveLength(1)
|
|
@@ -221,7 +313,7 @@ describe("pagination", () => {
|
|
|
221
313
|
// The old heuristic was "fewer rows than the limit means the end", which mistakes a final page
|
|
222
314
|
// that happens to be exactly full for a middle one. The node omits the cursor instead.
|
|
223
315
|
const { client } = makeClient(page(undefined, "0x1", "0x2"))
|
|
224
|
-
const result = await new SelectQueryBuilder(client, { key: true })
|
|
316
|
+
const result = await new SelectQueryBuilder(client, { $key: true })
|
|
225
317
|
.where(eq("a", 1))
|
|
226
318
|
.limit(2)
|
|
227
319
|
.fetch()
|
|
@@ -231,7 +323,7 @@ describe("pagination", () => {
|
|
|
231
323
|
|
|
232
324
|
it("pages the query it started with, not whatever the builder says later", async () => {
|
|
233
325
|
const { client, request } = makeClient(page("b64:1", "0x1"), page(undefined, "0x2"))
|
|
234
|
-
const builder = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))
|
|
326
|
+
const builder = new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1))
|
|
235
327
|
|
|
236
328
|
const first = await builder.fetch()
|
|
237
329
|
// The builder stays mutable and reusable. Re-reading it per page would send page 2 under a
|
|
@@ -248,8 +340,8 @@ describe("pagination", () => {
|
|
|
248
340
|
it("walks every page through the async iterator", async () => {
|
|
249
341
|
const { client } = makeClient(page("b64:1", "0x1", "0x2"), page(undefined, "0x3"))
|
|
250
342
|
const keys: unknown[] = []
|
|
251
|
-
for await (const entity of new SelectQueryBuilder(client, { key: true }).where(eq("a", 1))) {
|
|
252
|
-
keys.push((entity as { key: string })
|
|
343
|
+
for await (const entity of new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1))) {
|
|
344
|
+
keys.push((entity as { $key: string }).$key)
|
|
253
345
|
}
|
|
254
346
|
expect(keys).toEqual(["0x1", "0x2", "0x3"])
|
|
255
347
|
})
|
|
@@ -270,7 +362,7 @@ describe("node rejections", () => {
|
|
|
270
362
|
const data = { position: 14, expected: "typed literal", got: "3.5" }
|
|
271
363
|
;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(-32001, data))
|
|
272
364
|
|
|
273
|
-
const promise = new SelectQueryBuilder(client, { key: true }).where(eq("a", 1)).fetch()
|
|
365
|
+
const promise = new SelectQueryBuilder(client, { $key: true }).where(eq("a", 1)).fetch()
|
|
274
366
|
await expect(promise).rejects.toThrow(QueryError)
|
|
275
367
|
|
|
276
368
|
const error = (await promise.catch((e) => e)) as QueryError
|
|
@@ -293,7 +385,7 @@ describe("node rejections", () => {
|
|
|
293
385
|
for (const [code, kind] of Object.entries(kinds)) {
|
|
294
386
|
const { client } = makeClient()
|
|
295
387
|
;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(rpcFailure(Number(code)))
|
|
296
|
-
const error = await new SelectQueryBuilder(client, { key: true })
|
|
388
|
+
const error = await new SelectQueryBuilder(client, { $key: true })
|
|
297
389
|
.where(eq("a", 1))
|
|
298
390
|
.fetch()
|
|
299
391
|
.catch((e) => e)
|
|
@@ -306,7 +398,7 @@ describe("node rejections", () => {
|
|
|
306
398
|
const transportFailure = new Error("fetch failed")
|
|
307
399
|
;(client.request as ReturnType<typeof vi.fn>).mockRejectedValue(transportFailure)
|
|
308
400
|
|
|
309
|
-
const error = await new SelectQueryBuilder(client, { key: true })
|
|
401
|
+
const error = await new SelectQueryBuilder(client, { $key: true })
|
|
310
402
|
.where(eq("a", 1))
|
|
311
403
|
.fetch()
|
|
312
404
|
.catch((e) => e)
|
|
@@ -319,50 +411,367 @@ describe("the projected type", () => {
|
|
|
319
411
|
it("narrows the entity to exactly the selected fields", async () => {
|
|
320
412
|
const { client } = makeClient({
|
|
321
413
|
blockNumber: "0x1",
|
|
322
|
-
data: [{ key: KEY, owner: ACCOUNT
|
|
414
|
+
data: [{ key: KEY, owner: ACCOUNT }],
|
|
323
415
|
})
|
|
324
416
|
|
|
325
|
-
const page = await new SelectQueryBuilder<
|
|
326
|
-
key: true,
|
|
327
|
-
owner: true,
|
|
417
|
+
const page = await new SelectQueryBuilder<{ $key: true; $owner: true }>(client, {
|
|
418
|
+
$key: true,
|
|
419
|
+
$owner: true,
|
|
328
420
|
})
|
|
329
421
|
.where(eq("a", 1))
|
|
330
422
|
.fetch()
|
|
331
423
|
|
|
332
424
|
const entity = page.entities[0]
|
|
333
|
-
expectType<Hex>(entity
|
|
334
|
-
expectType<Hex>(entity
|
|
425
|
+
expectType<Hex>(entity.$key)
|
|
426
|
+
expectType<Hex>(entity.$owner)
|
|
335
427
|
// @ts-expect-error - creator was not selected
|
|
336
|
-
entity
|
|
337
|
-
// @ts-expect-error - toJson comes with the payload, which was not selected
|
|
338
|
-
entity.toJson()
|
|
428
|
+
entity.$creator
|
|
339
429
|
})
|
|
340
430
|
|
|
341
|
-
it("
|
|
342
|
-
const { client } = makeClient({
|
|
343
|
-
|
|
344
|
-
|
|
431
|
+
it("reads every attribute as a map with $attributes", async () => {
|
|
432
|
+
const { client } = makeClient({
|
|
433
|
+
blockNumber: "0x1",
|
|
434
|
+
data: [{ key: KEY, attributes: [{ name: "version", type: "i32", value: 4 }] }],
|
|
435
|
+
})
|
|
436
|
+
const page = await new SelectQueryBuilder<{ $key: true; $attributes: true }>(client, {
|
|
437
|
+
$key: true,
|
|
438
|
+
$attributes: true,
|
|
345
439
|
})
|
|
346
440
|
.where(eq("a", 1))
|
|
347
441
|
.fetch()
|
|
348
442
|
|
|
349
|
-
|
|
350
|
-
|
|
443
|
+
expectType<Attributes>(page.entities[0].$attributes)
|
|
444
|
+
expect(page.entities[0].$attributes).toEqual({ version: { type: "i32", value: 4 } })
|
|
445
|
+
})
|
|
446
|
+
})
|
|
447
|
+
|
|
448
|
+
describe("through a schema", () => {
|
|
449
|
+
/** Builds the way `client.select` does, so the selection is inferred as a literal. */
|
|
450
|
+
const select = <const S extends Selection>(client: ArkivClient, selection: S) =>
|
|
451
|
+
new SelectQueryBuilder<S>(client, selection)
|
|
452
|
+
|
|
453
|
+
const Posts = defineSchema({
|
|
454
|
+
author: addr().indexed(),
|
|
455
|
+
category: str().indexed(),
|
|
456
|
+
score: dec().indexed(),
|
|
457
|
+
title: str(),
|
|
458
|
+
body: bytes(),
|
|
459
|
+
thumbnail: bytes().optional(),
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
const row: RpcEntity = {
|
|
463
|
+
key: KEY,
|
|
464
|
+
owner: ACCOUNT,
|
|
465
|
+
attributes: [
|
|
466
|
+
{ name: "author", type: "addr", value: ACCOUNT },
|
|
467
|
+
{ name: "title", type: "str", value: "Hello" },
|
|
468
|
+
{ name: "body", type: "bytes", value: "0x2348" },
|
|
469
|
+
],
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
it("asks for the selected columns by name, alongside the entity fields", async () => {
|
|
473
|
+
const { client, request } = makeClient()
|
|
474
|
+
await select(client, { $key: true, title: true, body: true })
|
|
475
|
+
.from(Posts)
|
|
476
|
+
.where(eq("category", "docs"))
|
|
477
|
+
.fetch()
|
|
478
|
+
expect(sent(request).options.select).toMatchObject({
|
|
479
|
+
key: true,
|
|
480
|
+
owner: false,
|
|
481
|
+
attributes: { title: true, body: true },
|
|
482
|
+
})
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it("returns flat rows with each column typed by the schema", async () => {
|
|
486
|
+
const { client } = makeClient({ blockNumber: "0x1", data: [row] })
|
|
487
|
+
const page = await select(client, {
|
|
488
|
+
$key: true,
|
|
489
|
+
author: true,
|
|
490
|
+
title: true,
|
|
491
|
+
body: true,
|
|
492
|
+
thumbnail: true,
|
|
493
|
+
})
|
|
494
|
+
.from(Posts)
|
|
495
|
+
.where(eq("category", "docs"))
|
|
496
|
+
.fetch()
|
|
497
|
+
|
|
498
|
+
const [first] = page.entities
|
|
499
|
+
expect(first).toEqual({
|
|
500
|
+
$key: KEY,
|
|
501
|
+
author: addr(ACCOUNT),
|
|
502
|
+
title: str("Hello"),
|
|
503
|
+
body: bytes("0x2348"),
|
|
504
|
+
thumbnail: undefined,
|
|
505
|
+
})
|
|
506
|
+
expectType<Hex>(first.$key)
|
|
507
|
+
expectType<AddrValue>(first.author)
|
|
508
|
+
expectType<StrValue>(first.title)
|
|
509
|
+
expectType<BytesValue>(first.body)
|
|
510
|
+
expectType<BytesValue | undefined>(first.thumbnail)
|
|
511
|
+
// @ts-expect-error - score was not selected
|
|
512
|
+
first.score
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it("decodes every page of a walk as rows", async () => {
|
|
516
|
+
const { client } = makeClient(
|
|
517
|
+
{ blockNumber: "0x1", data: [row], cursor: "next" },
|
|
518
|
+
{ blockNumber: "0x1", data: [row] },
|
|
519
|
+
)
|
|
520
|
+
const titles: string[] = []
|
|
521
|
+
for await (const entry of select(client, { title: true })
|
|
522
|
+
.from(Posts)
|
|
523
|
+
.where(eq("category", "docs"))) {
|
|
524
|
+
titles.push(entry.title.value)
|
|
525
|
+
}
|
|
526
|
+
expect(titles).toEqual(["Hello", "Hello"])
|
|
351
527
|
})
|
|
352
528
|
|
|
353
|
-
it("
|
|
529
|
+
it("fails the fetch on an entity that does not fit the schema, with onMismatch: throw", async () => {
|
|
354
530
|
const { client } = makeClient({
|
|
355
531
|
blockNumber: "0x1",
|
|
356
|
-
data: [{ key: KEY, attributes: [{ name: "
|
|
532
|
+
data: [{ key: KEY, attributes: [{ name: "author", type: "addr", value: ACCOUNT }] }],
|
|
357
533
|
})
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
534
|
+
await expect(
|
|
535
|
+
select(client, { $key: true, title: true })
|
|
536
|
+
.from(Posts, { onMismatch: "throw" })
|
|
537
|
+
.where(eq("category", "docs"))
|
|
538
|
+
.fetch(),
|
|
539
|
+
).rejects.toThrow(SchemaMismatchError)
|
|
540
|
+
})
|
|
541
|
+
|
|
542
|
+
it("checks the filters against the schema before sending anything", async () => {
|
|
543
|
+
const { client, request } = makeClient()
|
|
544
|
+
const builder = select(client, { $key: true }).from(Posts)
|
|
545
|
+
// @ts-expect-error - title is not indexed
|
|
546
|
+
await expect(builder.where(eq("title", "Hello")).fetch()).rejects.toThrow(/not indexed/)
|
|
547
|
+
const bytesFilter = select(client, { $key: true }).from(Posts)
|
|
548
|
+
// @ts-expect-error - body is bytes, which is never indexed
|
|
549
|
+
await expect(bytesFilter.where(exists("body")).fetch()).rejects.toThrow(/never indexed/)
|
|
550
|
+
expect(request).not.toHaveBeenCalled()
|
|
551
|
+
})
|
|
552
|
+
|
|
553
|
+
it("rejects a selection naming something the schema does not have, at from()", () => {
|
|
554
|
+
const { client } = makeClient()
|
|
555
|
+
expect(() =>
|
|
556
|
+
select(client, { $key: true, nope: true })
|
|
557
|
+
// @ts-expect-error - nope is not a column
|
|
558
|
+
.from(Posts),
|
|
559
|
+
).toThrow(/"nope", which is not a column/)
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it("checks a selection built at runtime when from() is called", () => {
|
|
563
|
+
const { client } = makeClient()
|
|
564
|
+
const selection: SelectArg = JSON.parse('{"$key": true, "nope": true}')
|
|
565
|
+
expect(() => new SelectQueryBuilder(client, selection).from(Posts)).toThrow(
|
|
566
|
+
/"nope", which is not a column/,
|
|
567
|
+
)
|
|
568
|
+
})
|
|
569
|
+
|
|
570
|
+
it("sends the selection as it was when select() was called", async () => {
|
|
571
|
+
const { client, request } = makeClient()
|
|
572
|
+
const selection: Record<string, boolean> = { $key: true, title: true }
|
|
573
|
+
const builder = new SelectQueryBuilder(client, selection as Selection).from(Posts)
|
|
574
|
+
selection.author = true
|
|
575
|
+
await builder.where(eq("category", "docs")).fetch()
|
|
576
|
+
expect(sent(request).options.select).toMatchObject({ attributes: { title: true } })
|
|
577
|
+
})
|
|
578
|
+
|
|
579
|
+
it("carries the filters, owner, limit, cursor and block across from()", async () => {
|
|
580
|
+
const { client, request } = makeClient()
|
|
581
|
+
await new SelectQueryBuilder(client, { $key: true })
|
|
582
|
+
.where(eq("category", "docs"))
|
|
583
|
+
.ownedBy(ACCOUNT)
|
|
584
|
+
.limit(5)
|
|
585
|
+
.cursor("0xc0")
|
|
586
|
+
.atBlock(7n)
|
|
587
|
+
.from(Posts)
|
|
588
|
+
.fetch()
|
|
589
|
+
const { query, options } = sent(request)
|
|
590
|
+
expect(query).toBe(`category = str('docs') AND $owner = addr(${ACCOUNT})`)
|
|
591
|
+
expect(options).toMatchObject({ limit: "0x5", cursor: "0xc0", atBlock: "0x7" })
|
|
592
|
+
})
|
|
593
|
+
|
|
594
|
+
it("reads the same selection without a schema, untyped and unchecked", async () => {
|
|
595
|
+
const misfit = { key: KEY, attributes: [{ name: "title", type: "i32", value: 7 }] }
|
|
596
|
+
const { client, request } = makeClient({ blockNumber: "0x1", data: [row, misfit] })
|
|
597
|
+
const page = await select(client, { $key: true, title: true, body: true })
|
|
598
|
+
.where(eq("category", "docs"))
|
|
599
|
+
.fetch()
|
|
600
|
+
|
|
601
|
+
expect(sent(request).options.select).toMatchObject({
|
|
602
|
+
key: true,
|
|
603
|
+
attributes: { title: true, body: true },
|
|
604
|
+
})
|
|
605
|
+
// The same flat rows as through a schema; without one, nothing is checked, so the misfit
|
|
606
|
+
// comes back as it is and a missing attribute as undefined.
|
|
607
|
+
expect(page.entities).toEqual([
|
|
608
|
+
{ $key: KEY, title: str("Hello"), body: bytes("0x2348") },
|
|
609
|
+
{ $key: KEY, title: i32(7), body: undefined },
|
|
610
|
+
])
|
|
611
|
+
const title: ArkivValue | undefined = page.entities[0]?.title
|
|
612
|
+
expect(title).toEqual(str("Hello"))
|
|
613
|
+
})
|
|
614
|
+
|
|
615
|
+
it("skips a mismatch by default, and throws on it with onMismatch: throw", async () => {
|
|
616
|
+
// Anyone can write an entity a query matches but the schema does not fit, so one such entity
|
|
617
|
+
// must not fail every read of the others unless the caller asks for that.
|
|
618
|
+
const misfit = { key: KEY, attributes: [{ name: "author", type: "addr", value: ACCOUNT }] }
|
|
619
|
+
const pages = () => makeClient({ blockNumber: "0x1", data: [row, misfit, row] }).client
|
|
620
|
+
|
|
621
|
+
const page = await select(pages(), { $key: true, title: true })
|
|
622
|
+
.from(Posts)
|
|
623
|
+
.where(eq("category", "docs"))
|
|
624
|
+
.fetch()
|
|
625
|
+
expect(page.entities.map((entry) => entry.title.value)).toEqual(["Hello", "Hello"])
|
|
626
|
+
expect(page.skipped).toBe(1)
|
|
627
|
+
|
|
628
|
+
await expect(
|
|
629
|
+
select(pages(), { $key: true, title: true })
|
|
630
|
+
.from(Posts, { onMismatch: "throw" })
|
|
631
|
+
.where(eq("category", "docs"))
|
|
632
|
+
.fetch(),
|
|
633
|
+
).rejects.toThrow(SchemaMismatchError)
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it("still throws errors that are not mismatches when skipping", async () => {
|
|
637
|
+
const { client } = makeClient({
|
|
638
|
+
blockNumber: "0x1",
|
|
639
|
+
data: [{ key: KEY, attributes: [{ name: "title", type: "nope", value: 1 }] }],
|
|
640
|
+
})
|
|
641
|
+
await expect(
|
|
642
|
+
select(client, { title: true })
|
|
643
|
+
.from(Posts, { onMismatch: "skip" })
|
|
644
|
+
.where(eq("category", "docs"))
|
|
645
|
+
.fetch(),
|
|
646
|
+
).rejects.toThrow(/Unknown attribute type/)
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
it("skips on every page of a walk", async () => {
|
|
650
|
+
const misfit = { key: KEY, attributes: [] }
|
|
651
|
+
const { client } = makeClient(
|
|
652
|
+
{ blockNumber: "0x1", data: [misfit], cursor: "next" },
|
|
653
|
+
{ blockNumber: "0x1", data: [row] },
|
|
654
|
+
)
|
|
655
|
+
const titles: string[] = []
|
|
656
|
+
for await (const entry of select(client, { title: true })
|
|
657
|
+
.from(Posts, { onMismatch: "skip" })
|
|
658
|
+
.where(eq("category", "docs"))) {
|
|
659
|
+
titles.push(entry.title.value)
|
|
660
|
+
}
|
|
661
|
+
expect(titles).toEqual(["Hello"])
|
|
662
|
+
})
|
|
663
|
+
|
|
664
|
+
it("counts skips per page, so an empty page with a cursor is not mistaken for the end", async () => {
|
|
665
|
+
const misfit = { key: KEY, attributes: [] }
|
|
666
|
+
const { client } = makeClient(
|
|
667
|
+
{ blockNumber: "0x1", data: [misfit, misfit], cursor: "next" },
|
|
668
|
+
{ blockNumber: "0x1", data: [row] },
|
|
669
|
+
)
|
|
670
|
+
const first = await select(client, { title: true })
|
|
671
|
+
.from(Posts, { onMismatch: "skip" })
|
|
672
|
+
.where(eq("category", "docs"))
|
|
673
|
+
.fetch()
|
|
674
|
+
expect(first.entities).toEqual([])
|
|
675
|
+
expect(first.skipped).toBe(2)
|
|
676
|
+
expect(first.hasNextPage()).toBe(true)
|
|
677
|
+
|
|
678
|
+
const second = await first.next()
|
|
679
|
+
expect(second.entities).toHaveLength(1)
|
|
680
|
+
expect(second.skipped).toBe(0)
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
it("reports nothing skipped when not skipping, or without a schema", async () => {
|
|
684
|
+
const throughSchema = await select(makeClient({ blockNumber: "0x1", data: [row] }).client, {
|
|
685
|
+
title: true,
|
|
686
|
+
})
|
|
687
|
+
.from(Posts)
|
|
688
|
+
.where(eq("category", "docs"))
|
|
689
|
+
.fetch()
|
|
690
|
+
expect(throughSchema.skipped).toBe(0)
|
|
691
|
+
|
|
692
|
+
const plain = await new SelectQueryBuilder(makeClient().client, { $key: true })
|
|
693
|
+
.where(eq("category", "docs"))
|
|
362
694
|
.fetch()
|
|
695
|
+
expect(plain.skipped).toBe(0)
|
|
696
|
+
})
|
|
697
|
+
|
|
698
|
+
it("leaves the builder it was called on untouched, so two schemas never share one", async () => {
|
|
699
|
+
const Users = defineSchema("users", { category: str().indexed(), name: str() })
|
|
700
|
+
const { client, request } = makeClient()
|
|
701
|
+
const base = new SelectQueryBuilder(client, "*").limit(5)
|
|
702
|
+
const posts = base.from(Posts).where(eq("category", "docs"))
|
|
703
|
+
const users = base.from(Users)
|
|
704
|
+
|
|
705
|
+
expect(posts).not.toBe(base)
|
|
706
|
+
expect(users).not.toBe(posts)
|
|
707
|
+
await users.where(eq("category", "people")).fetch()
|
|
708
|
+
await posts.fetch()
|
|
709
|
+
// Each carried what `base` held when it was made, and nothing the other added since.
|
|
710
|
+
expect(sent(request, 0).query).toBe("arkiv.schema = str('users') AND category = str('people')")
|
|
711
|
+
expect(sent(request, 0).options.limit).toBe("0x5")
|
|
712
|
+
expect(sent(request, 1).query).toBe("category = str('docs')")
|
|
713
|
+
expect(() => base.toString()).toThrow(/at least one filter/)
|
|
714
|
+
})
|
|
715
|
+
|
|
716
|
+
it("types a dynamic selection's rows with every field optional", async () => {
|
|
717
|
+
const { client } = makeClient({ blockNumber: "0x1", data: [row] })
|
|
718
|
+
const selection: SelectArg = { $key: true }
|
|
719
|
+
const page = await new SelectQueryBuilder<DynamicSelection>(client, selection)
|
|
720
|
+
.from(Posts)
|
|
721
|
+
.where(eq("category", "docs"))
|
|
722
|
+
.fetch()
|
|
723
|
+
const [first] = page.entities
|
|
724
|
+
expectType<StrValue | undefined>(first.title)
|
|
725
|
+
expect(first).toEqual({ $key: KEY })
|
|
726
|
+
})
|
|
727
|
+
|
|
728
|
+
it("rejects an onMismatch it does not know", () => {
|
|
729
|
+
const { client } = makeClient()
|
|
730
|
+
expect(() =>
|
|
731
|
+
select(client, { title: true }).from(Posts, { onMismatch: "ignore" as never }),
|
|
732
|
+
).toThrow(/"throw" or "skip"/)
|
|
733
|
+
})
|
|
734
|
+
|
|
735
|
+
it("rejects something that is not a schema", () => {
|
|
736
|
+
const { client } = makeClient()
|
|
737
|
+
expect(() =>
|
|
738
|
+
new SelectQueryBuilder(client, "*").from({ columns: {} } as unknown as typeof Posts),
|
|
739
|
+
).toThrow(/defineSchema/)
|
|
740
|
+
})
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
describe("through a named schema", () => {
|
|
744
|
+
const Posts = defineSchema("posts", { category: str().indexed(), title: str() })
|
|
745
|
+
const select = <const S extends Selection>(client: ArkivClient, selection: S) =>
|
|
746
|
+
new SelectQueryBuilder<S>(client, selection)
|
|
747
|
+
|
|
748
|
+
it("filters on the schema's tag, ahead of everything else", async () => {
|
|
749
|
+
const { client, request } = makeClient()
|
|
750
|
+
await select(client, { $key: true }).from(Posts).where(eq("category", "docs")).fetch()
|
|
751
|
+
expect(sent(request).query).toBe("arkiv.schema = str('posts') AND category = str('docs')")
|
|
752
|
+
})
|
|
363
753
|
|
|
364
|
-
|
|
365
|
-
|
|
754
|
+
it("needs no other filter, so it can list every entity of the schema", async () => {
|
|
755
|
+
const { client, request } = makeClient()
|
|
756
|
+
await select(client, { $key: true }).from(Posts).fetch()
|
|
757
|
+
expect(sent(request).query).toBe("arkiv.schema = str('posts')")
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
it("checks the caller's filters, not the tag it adds itself", async () => {
|
|
761
|
+
const { client } = makeClient()
|
|
762
|
+
const builder = select(client, { $key: true }).from(Posts)
|
|
763
|
+
// @ts-expect-error - arkiv.schema is not a column
|
|
764
|
+
builder.where(eq("arkiv.schema", "comments"))
|
|
765
|
+
await expect(builder.fetch()).rejects.toThrow(/"arkiv.schema" is not a column/)
|
|
766
|
+
})
|
|
767
|
+
|
|
768
|
+
it("leaves an unnamed schema's query to its filters alone", async () => {
|
|
769
|
+
const { client, request } = makeClient()
|
|
770
|
+
await select(client, { $key: true })
|
|
771
|
+
.from(defineSchema({ category: str().indexed() }))
|
|
772
|
+
.where(eq("category", "docs"))
|
|
773
|
+
.fetch()
|
|
774
|
+
expect(sent(request).query).toBe("category = str('docs')")
|
|
366
775
|
})
|
|
367
776
|
})
|
|
368
777
|
|