@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,101 +1,159 @@
|
|
|
1
|
-
import type { Hex } from "viem"
|
|
2
|
-
import { addr } from "../attr"
|
|
3
1
|
import type { ArkivClient } from "../clients/baseClient"
|
|
2
|
+
import { isSchema, type Schema, SchemaDefinitionError, SchemaMismatchError } from "../schema"
|
|
4
3
|
import type { Entity } from "../types/entity"
|
|
5
|
-
import
|
|
4
|
+
import { QueryBuilderBase } from "./builderBase"
|
|
6
5
|
import { type QueryRequest, runQuery } from "./engine"
|
|
7
|
-
import {
|
|
8
|
-
import { and, type Expression, eq, type or } from "./expression"
|
|
6
|
+
import type { or } from "./expression"
|
|
9
7
|
import { QueryResult } from "./queryResult"
|
|
10
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type Projection,
|
|
10
|
+
projectSelection,
|
|
11
|
+
type ResultOf,
|
|
12
|
+
type SelectArg,
|
|
13
|
+
snapshotSelection,
|
|
14
|
+
toRow,
|
|
15
|
+
type UnknownSelection,
|
|
16
|
+
} from "./selection"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* `from(schema)`'s check that every selected key is an entity field or a column — `unknown` when
|
|
20
|
+
* it holds, and an object naming the offenders, which no schema satisfies, when it does not.
|
|
21
|
+
*/
|
|
22
|
+
type SelectionCheck<TSelection, TSchema extends Schema> = [
|
|
23
|
+
UnknownSelection<TSelection, TSchema>,
|
|
24
|
+
] extends [never]
|
|
25
|
+
? unknown
|
|
26
|
+
: { "selected names that are not columns of this schema": UnknownSelection<TSelection, TSchema> }
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* How a query through a schema treats a matching entity that does not fit the schema — one not
|
|
30
|
+
* tagged with a named schema's name, or missing a selected required column or holding one of
|
|
31
|
+
* another type. Columns the selection leaves out are not checked.
|
|
32
|
+
*/
|
|
33
|
+
export type FromOptions = {
|
|
34
|
+
/**
|
|
35
|
+
* `"skip"` (the default) leaves the entity out of the page — so a page can hold fewer rows than
|
|
36
|
+
* its limit, and a page whose every entity was skipped is empty while later pages still have rows.
|
|
37
|
+
* Each page reports how many it left out as {@link QueryResult.skipped}. Arkiv is public, so
|
|
38
|
+
* anyone can write an entity that matches a query but not the schema; skipping keeps one such
|
|
39
|
+
* entity from failing every read of the others. `"throw"` fails the fetch with a
|
|
40
|
+
* {@link SchemaMismatchError} instead.
|
|
41
|
+
*/
|
|
42
|
+
onMismatch?: "throw" | "skip" | undefined
|
|
43
|
+
}
|
|
11
44
|
|
|
12
45
|
/**
|
|
13
46
|
* Builds and runs a query.
|
|
14
47
|
*
|
|
15
48
|
* The selection is fixed when the builder is created and the filter is added by chaining, so the
|
|
16
49
|
* result type is known before a single predicate is written. Create one with `client.select(...)`
|
|
17
|
-
* rather than constructing it directly — that is what infers the
|
|
50
|
+
* rather than constructing it directly — that is what infers the result shape from the selection.
|
|
51
|
+
*
|
|
52
|
+
* The query returns flat rows: the selected entity fields and attributes side by side. Naming a
|
|
53
|
+
* schema with {@link from} changes what the selection means for nothing but the selected
|
|
54
|
+
* attributes — they come back typed and checked as the schema declares — and has `where` checked
|
|
55
|
+
* against it, so that filtering on a column the schema does not index, or comparing one against the
|
|
56
|
+
* wrong type, is a compile error. A named schema also restricts the query to entities tagged with
|
|
57
|
+
* its name, and leaves that tag out of the rows' `$attributes` and `$attributeSchema`.
|
|
18
58
|
*
|
|
19
59
|
* Every filter added with {@link where}, {@link ownedBy} and {@link createdBy} must hold; use
|
|
20
60
|
* {@link or} for alternatives inside one call.
|
|
21
61
|
*
|
|
22
|
-
* @typeParam
|
|
62
|
+
* @typeParam TSelection - The selection, inferred by `client.select()`.
|
|
63
|
+
* @typeParam TSchema - The schema named with {@link from}, if any.
|
|
23
64
|
*
|
|
24
65
|
* @example
|
|
25
66
|
* import { createPublicClient } from "@arkiv-network/sdk"
|
|
26
|
-
* import {
|
|
67
|
+
* import { dec } from "@arkiv-network/sdk/schema"
|
|
27
68
|
* import { tiramisu } from "@arkiv-network/sdk/chains"
|
|
28
|
-
* import {
|
|
69
|
+
* import { and, eq, gt } from "@arkiv-network/sdk/query"
|
|
29
70
|
* import { http } from "viem"
|
|
30
71
|
*
|
|
31
72
|
* const client = createPublicClient({ chain: tiramisu, transport: http() })
|
|
32
73
|
*
|
|
33
74
|
* const page = await client
|
|
34
|
-
* .select({ key: true,
|
|
35
|
-
* .
|
|
36
|
-
* .
|
|
37
|
-
* .limit(
|
|
75
|
+
* .select({ $key: true, author: true, title: true })
|
|
76
|
+
* .from(Posts)
|
|
77
|
+
* .where(and(eq("category", "docs"), gt("score", dec("4.0"))))
|
|
78
|
+
* .limit(20)
|
|
38
79
|
* .fetch()
|
|
39
80
|
*
|
|
40
|
-
* for (const
|
|
81
|
+
* for (const row of page.entities) console.log(row.$key, row.title.value)
|
|
41
82
|
*/
|
|
42
|
-
export class SelectQueryBuilder<
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
private
|
|
83
|
+
export class SelectQueryBuilder<
|
|
84
|
+
TSelection = "*",
|
|
85
|
+
TSchema extends Schema | undefined = undefined,
|
|
86
|
+
TCount extends bigint | undefined = undefined,
|
|
87
|
+
> extends QueryBuilderBase<TSchema> {
|
|
88
|
+
private readonly _selection: SelectArg | undefined
|
|
89
|
+
private _projection: Projection
|
|
90
|
+
private _onMismatch: "throw" | "skip" = "skip"
|
|
48
91
|
private _limit: number | undefined
|
|
49
92
|
private _cursor: string | undefined
|
|
50
|
-
private
|
|
93
|
+
private _withCount = false
|
|
51
94
|
|
|
95
|
+
/**
|
|
96
|
+
* @throws If the selection is not `"*"` or an object, asks for nothing, or names an attribute
|
|
97
|
+
* outside the name grammar.
|
|
98
|
+
*/
|
|
52
99
|
constructor(client: ArkivClient, selection?: SelectArg) {
|
|
53
|
-
|
|
54
|
-
this
|
|
100
|
+
super(client)
|
|
101
|
+
// A copy: the caller's object may change after this, and the builder is typed for it as it was.
|
|
102
|
+
this._selection = snapshotSelection(selection)
|
|
103
|
+
this._projection = projectSelection(this._selection)
|
|
55
104
|
}
|
|
56
105
|
|
|
57
106
|
/**
|
|
58
|
-
*
|
|
107
|
+
* Runs the query through a schema. The selection keeps its meaning and the rows their shape;
|
|
108
|
+
* each selected attribute becomes a column — typed as the schema declares, and checked against
|
|
109
|
+
* it — and `where` is checked against the schema's indexed columns.
|
|
59
110
|
*
|
|
60
|
-
*
|
|
111
|
+
* A named schema also filters: only entities tagged with its name match, so a query through it
|
|
112
|
+
* sees only entities written through it, and needs no other filter. The tag is how the query finds
|
|
113
|
+
* them rather than one of their attributes, so rows leave it out of `$attributes` and
|
|
114
|
+
* `$attributeSchema`. An unnamed schema types the query without filtering it — `where` alone
|
|
115
|
+
* decides which entities match.
|
|
61
116
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*/
|
|
67
|
-
where(expressions: readonly Expression[]): this
|
|
68
|
-
where(...expressions: Expression[]): this
|
|
69
|
-
where(...expressions: (Expression | readonly Expression[])[]): this {
|
|
70
|
-
this._filters.push(...expressions.flat())
|
|
71
|
-
return this
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Restricts the results to entities this account owns — shorthand for
|
|
76
|
-
* `where(eq("$owner", addr(owner)))`. Calling it again replaces the filter.
|
|
117
|
+
* A matching entity that does not fit — a selected required column missing or of another type,
|
|
118
|
+
* or, for a named schema, a tag that does not match — is left out of the page, or fails the fetch
|
|
119
|
+
* with a {@link SchemaMismatchError} with `{ onMismatch: "throw" }`. Columns the selection leaves
|
|
120
|
+
* out are not checked.
|
|
77
121
|
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* Restricts the results to entities this account created — shorthand for
|
|
88
|
-
* `where(eq("$creator", addr(creator)))`. Calling it again replaces the filter.
|
|
122
|
+
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
|
|
123
|
+
*
|
|
124
|
+
* @param schema - The schema, built with `defineSchema`.
|
|
125
|
+
* @param options - How to treat entities that do not fit. {@link FromOptions}
|
|
126
|
+
* @throws {SchemaDefinitionError} If `schema` was not built with `defineSchema`.
|
|
127
|
+
* @throws If the selection names something that is neither an entity field nor a column.
|
|
89
128
|
*
|
|
90
|
-
*
|
|
129
|
+
* @example
|
|
130
|
+
* client.select({ $key: true, title: true }).from(Posts) // rows typed { $key; title }
|
|
131
|
+
* client.select().from(Posts) // every field and every column
|
|
132
|
+
* client.select().from(Posts, { onMismatch: "throw" }) // fail on entities that do not fit
|
|
91
133
|
*/
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
134
|
+
from<S extends Schema>(
|
|
135
|
+
schema: S & SelectionCheck<TSelection, S>,
|
|
136
|
+
options: FromOptions = {},
|
|
137
|
+
): SelectQueryBuilder<TSelection, S, TCount> {
|
|
138
|
+
if (!isSchema(schema)) {
|
|
139
|
+
throw new SchemaDefinitionError("from() takes a schema built with defineSchema(...)")
|
|
140
|
+
}
|
|
141
|
+
const { onMismatch = "skip" } = options
|
|
142
|
+
if (onMismatch !== "throw" && onMismatch !== "skip") {
|
|
143
|
+
throw new TypeError(`onMismatch must be "throw" or "skip", got ${String(onMismatch)}`)
|
|
144
|
+
}
|
|
145
|
+
// Resolved here rather than at fetch time, so a selection naming something that is neither an
|
|
146
|
+
// entity field nor a column fails at the call that made it wrong.
|
|
147
|
+
const projection = projectSelection(this._selection, schema)
|
|
148
|
+
const typed = this.copy<SelectQueryBuilder<TSelection, S, TCount>>()
|
|
149
|
+
typed._schema = schema
|
|
150
|
+
typed._projection = projection
|
|
151
|
+
typed._onMismatch = onMismatch
|
|
152
|
+
return typed
|
|
95
153
|
}
|
|
96
154
|
|
|
97
155
|
/**
|
|
98
|
-
* Sets the page size, up to the node maximum of
|
|
156
|
+
* Sets the page size, up to the node maximum of 100. Without it the node picks the page size.
|
|
99
157
|
*
|
|
100
158
|
* @example
|
|
101
159
|
* builder.limit(100)
|
|
@@ -118,71 +176,89 @@ export class SelectQueryBuilder<TEntity = Entity> {
|
|
|
118
176
|
}
|
|
119
177
|
|
|
120
178
|
/**
|
|
121
|
-
*
|
|
122
|
-
* the
|
|
179
|
+
* Also counts every entity the query matches, regardless of the page size. Each page then carries
|
|
180
|
+
* the total as {@link QueryResult.count}, typed `bigint`, so a paginated list can show it without
|
|
181
|
+
* a second query. To count without fetching rows, use `client.count()` instead.
|
|
123
182
|
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
/**
|
|
133
|
-
* The query string this builder will send — the same text the node parses.
|
|
183
|
+
* The node counts what the filters match. Through a schema, that includes entities the pages
|
|
184
|
+
* leave out for not fitting it, so the rows of a whole walk can add up to less than the count:
|
|
185
|
+
* the difference is the sum of every page's {@link QueryResult.skipped}.
|
|
186
|
+
*
|
|
187
|
+
* @throws If, when fetched, the node returns no count — it predates the count projection.
|
|
188
|
+
*
|
|
189
|
+
* Returns a new builder, carrying whatever this one already holds; this one is left as it was.
|
|
134
190
|
*
|
|
135
|
-
* @
|
|
191
|
+
* @example
|
|
192
|
+
* const page = await builder.withCount().limit(20).fetch()
|
|
193
|
+
* page.count // every match, not just these 20
|
|
136
194
|
*/
|
|
137
|
-
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (filters.length === 0) {
|
|
142
|
-
throw new InvalidPredicateError(
|
|
143
|
-
"A query needs at least one filter — the language has no spelling for 'match every " +
|
|
144
|
-
'entity". Add a where(...) predicate, or narrow by ownedBy(...) / createdBy(...).',
|
|
145
|
-
)
|
|
146
|
-
}
|
|
147
|
-
return and(filters).toString()
|
|
195
|
+
withCount(): SelectQueryBuilder<TSelection, TSchema, bigint> {
|
|
196
|
+
const counted = this.copy<SelectQueryBuilder<TSelection, TSchema, bigint>>()
|
|
197
|
+
counted._withCount = true
|
|
198
|
+
return counted
|
|
148
199
|
}
|
|
149
200
|
|
|
150
201
|
/**
|
|
151
202
|
* Runs the query and returns the first page.
|
|
152
203
|
*
|
|
153
|
-
* @throws {InvalidPredicateError} If no filter has been added
|
|
204
|
+
* @throws {InvalidPredicateError} If no filter has been added, or a filter does not fit the
|
|
205
|
+
* schema.
|
|
206
|
+
* @throws If the {@link limit} is not an integer from 1 to 100.
|
|
207
|
+
* @throws {SchemaMismatchError} If, through a schema, a matching entity does not fit the selected
|
|
208
|
+
* columns or a named schema's tag, and `from()` was given `{ onMismatch: "throw" }`.
|
|
154
209
|
* @throws {QueryError} If the node rejects the query.
|
|
155
210
|
*
|
|
156
211
|
* @example
|
|
157
|
-
* const page = await client.select({ key: true }).where(eq("category", "docs")).fetch()
|
|
212
|
+
* const page = await client.select({ $key: true }).where(eq("category", "docs")).fetch()
|
|
158
213
|
* page.entities // this page
|
|
159
214
|
* await page.next() // the next one
|
|
160
215
|
*/
|
|
216
|
+
fetch(): Promise<QueryResult<ResultOf<TSelection, TSchema>, TCount>> {
|
|
217
|
+
return this.fetchFirstPage()
|
|
218
|
+
}
|
|
219
|
+
|
|
161
220
|
// Async so that a builder with no filter rejects like every other failure here, rather than
|
|
162
221
|
// throwing synchronously out of a call the caller is awaiting.
|
|
163
|
-
async
|
|
222
|
+
private async fetchFirstPage(): Promise<QueryResult<ResultOf<TSelection, TSchema>, TCount>> {
|
|
164
223
|
// Snapshot the request here, so the pages of one walk are all pages of the *same* query. The
|
|
165
224
|
// builder stays mutable and reusable; a cursor is bound to the query, block and selection it
|
|
166
225
|
// was issued for, so re-reading a since-edited builder would page with a mismatched cursor.
|
|
226
|
+
const projection = this._projection
|
|
167
227
|
const request = Object.freeze({
|
|
168
228
|
query: this.toString(),
|
|
169
|
-
select:
|
|
229
|
+
select: projection.select,
|
|
170
230
|
limit: this._limit,
|
|
171
231
|
atBlock: this._atBlock,
|
|
232
|
+
count: this._withCount,
|
|
172
233
|
})
|
|
173
|
-
|
|
234
|
+
// Every entity is laid out as a row of the selection — and, through a schema, checked against it.
|
|
235
|
+
// The static narrowing to the selected fields comes from `client.select()`'s inference and
|
|
236
|
+
// nothing at runtime depends on it.
|
|
237
|
+
const skip = this._onMismatch === "skip"
|
|
238
|
+
const decode = ((entity: Entity) => {
|
|
239
|
+
try {
|
|
240
|
+
return toRow(entity, projection)
|
|
241
|
+
} catch (error) {
|
|
242
|
+
if (skip && error instanceof SchemaMismatchError) return SKIPPED
|
|
243
|
+
throw error
|
|
244
|
+
}
|
|
245
|
+
}) as (entity: Entity) => ResultOf<TSelection, TSchema> | typeof SKIPPED
|
|
246
|
+
return fetchPage(this._client, request, this._cursor, decode) as Promise<
|
|
247
|
+
QueryResult<ResultOf<TSelection, TSchema>, TCount>
|
|
248
|
+
>
|
|
174
249
|
}
|
|
175
250
|
|
|
176
251
|
/**
|
|
177
|
-
* Walks every page, yielding one
|
|
252
|
+
* Walks every page, yielding one row at a time.
|
|
178
253
|
*
|
|
179
254
|
* @example
|
|
180
|
-
* for await (const
|
|
181
|
-
* console.log(
|
|
255
|
+
* for await (const row of client.select({ $key: true }).where(eq("category", "docs"))) {
|
|
256
|
+
* console.log(row.$key)
|
|
182
257
|
* }
|
|
183
258
|
*/
|
|
184
|
-
async *[Symbol.asyncIterator](): AsyncGenerator<
|
|
185
|
-
let page: QueryResult<
|
|
259
|
+
async *[Symbol.asyncIterator](): AsyncGenerator<ResultOf<TSelection, TSchema>> {
|
|
260
|
+
let page: QueryResult<ResultOf<TSelection, TSchema>, TCount> | undefined =
|
|
261
|
+
await this.fetchFirstPage()
|
|
186
262
|
while (page) {
|
|
187
263
|
yield* page.entities
|
|
188
264
|
page = page.hasNextPage() ? await page.next() : undefined
|
|
@@ -190,25 +266,42 @@ export class SelectQueryBuilder<TEntity = Entity> {
|
|
|
190
266
|
}
|
|
191
267
|
}
|
|
192
268
|
|
|
269
|
+
/** What `decode` returns for an entity `{ onMismatch: "skip" }` leaves out of its page. */
|
|
270
|
+
const SKIPPED: unique symbol = Symbol("skipped")
|
|
271
|
+
|
|
193
272
|
/**
|
|
194
273
|
* Fetches one page and wires up the next.
|
|
195
274
|
*
|
|
196
275
|
* Free-standing rather than a method: a page holds this closure for its lifetime, and closing over
|
|
197
276
|
* a frozen request keeps the builder — and everything it references — out of that reference.
|
|
198
277
|
*/
|
|
199
|
-
async function fetchPage<
|
|
278
|
+
async function fetchPage<TResult>(
|
|
200
279
|
client: ArkivClient,
|
|
201
280
|
request: Readonly<Omit<QueryRequest, "cursor">>,
|
|
202
281
|
cursor: string | undefined,
|
|
203
|
-
|
|
282
|
+
decode: (entity: Entity) => TResult | typeof SKIPPED,
|
|
283
|
+
): Promise<QueryResult<TResult>> {
|
|
204
284
|
const response = await runQuery(client, { ...request, cursor })
|
|
285
|
+
// withCount() types the count as present, so a node that leaves it out is an error, not a page
|
|
286
|
+
// whose count is silently undefined.
|
|
287
|
+
if (request.count && response.count === undefined) {
|
|
288
|
+
throw new Error("The node returned no count. It may predate the count projection.")
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const results: TResult[] = []
|
|
292
|
+
let skipped = 0
|
|
293
|
+
for (const entity of response.entities) {
|
|
294
|
+
const result = decode(entity)
|
|
295
|
+
if (result === SKIPPED) skipped++
|
|
296
|
+
else results.push(result)
|
|
297
|
+
}
|
|
205
298
|
|
|
206
|
-
return new QueryResult<
|
|
207
|
-
|
|
208
|
-
// `client.select()`'s inference and nothing at runtime depends on it.
|
|
209
|
-
response.entities as unknown as TEntity[],
|
|
299
|
+
return new QueryResult<TResult>(
|
|
300
|
+
results,
|
|
210
301
|
response.blockNumber,
|
|
211
302
|
response.cursor,
|
|
212
|
-
|
|
303
|
+
skipped,
|
|
304
|
+
response.count,
|
|
305
|
+
(next) => fetchPage(client, request, next, decode),
|
|
213
306
|
)
|
|
214
307
|
}
|
package/src/query/queryResult.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { NoMoreResultsError } from "../errors"
|
|
2
|
-
import type { Entity } from "../types/entity"
|
|
3
2
|
|
|
4
3
|
/** Fetches the page a cursor points at. */
|
|
5
|
-
type FetchPage<TEntity> = (
|
|
4
|
+
type FetchPage<TEntity, TCount extends bigint | undefined> = (
|
|
5
|
+
cursor: string,
|
|
6
|
+
) => Promise<QueryResult<TEntity, TCount>>
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* One page of query results.
|
|
@@ -10,16 +11,21 @@ type FetchPage<TEntity> = (cursor: string) => Promise<QueryResult<TEntity>>
|
|
|
10
11
|
* A page is immutable: {@link next} returns the following page rather than mutating this one, so a
|
|
11
12
|
* page that has been read stays readable.
|
|
12
13
|
*
|
|
13
|
-
* @typeParam TEntity - The shape of each
|
|
14
|
+
* @typeParam TEntity - The shape of each row, inferred from the selection by `client.select()`.
|
|
15
|
+
* @typeParam TCount - `bigint` when the query was built with `withCount()`, and `undefined` when it
|
|
16
|
+
* was not.
|
|
14
17
|
*
|
|
15
18
|
* @example
|
|
16
|
-
* let page = await client.select({ key: true }).where(eq("category", "docs")).limit(100).fetch()
|
|
19
|
+
* let page = await client.select({ $key: true }).where(eq("category", "docs")).limit(100).fetch()
|
|
17
20
|
* while (page.hasNextPage()) {
|
|
18
21
|
* page = await page.next()
|
|
19
22
|
* }
|
|
20
23
|
*/
|
|
21
|
-
export class QueryResult<
|
|
22
|
-
|
|
24
|
+
export class QueryResult<
|
|
25
|
+
TEntity = Record<string, unknown>,
|
|
26
|
+
TCount extends bigint | undefined = bigint | undefined,
|
|
27
|
+
> {
|
|
28
|
+
/** The rows on this page — flat, as the selection lays them out. */
|
|
23
29
|
readonly entities: readonly TEntity[]
|
|
24
30
|
/**
|
|
25
31
|
* The block this page was read at.
|
|
@@ -30,18 +36,38 @@ export class QueryResult<TEntity = Entity> {
|
|
|
30
36
|
readonly blockNumber: bigint
|
|
31
37
|
/** The cursor for the next page, or `undefined` when this was the last one. */
|
|
32
38
|
readonly cursor: string | undefined
|
|
39
|
+
/**
|
|
40
|
+
* How many matching entities this page left out because they did not fit the schema — always
|
|
41
|
+
* `0` without a schema, or with `.from(schema, { onMismatch: "throw" })`.
|
|
42
|
+
*
|
|
43
|
+
* Counted per page, not per walk: `entities.length + skipped` is what the node returned for this
|
|
44
|
+
* page, so a short or empty page with a cursor is explained here rather than mistaken for the
|
|
45
|
+
* end of the results.
|
|
46
|
+
*/
|
|
47
|
+
readonly skipped: number
|
|
48
|
+
/**
|
|
49
|
+
* How many entities the query matches in all, regardless of the page size — a `bigint` when the
|
|
50
|
+
* query was built with `withCount()`, and `undefined` otherwise. Every page of one walk reads the
|
|
51
|
+
* same block, so every page carries the same count. It includes entities left out for not fitting
|
|
52
|
+
* the schema, which {@link skipped} reports per page.
|
|
53
|
+
*/
|
|
54
|
+
readonly count: TCount
|
|
33
55
|
|
|
34
|
-
private readonly fetchPage: FetchPage<TEntity>
|
|
56
|
+
private readonly fetchPage: FetchPage<TEntity, TCount>
|
|
35
57
|
|
|
36
58
|
constructor(
|
|
37
59
|
entities: readonly TEntity[],
|
|
38
60
|
blockNumber: bigint,
|
|
39
61
|
cursor: string | undefined,
|
|
40
|
-
|
|
62
|
+
skipped: number,
|
|
63
|
+
count: TCount,
|
|
64
|
+
fetchPage: FetchPage<TEntity, TCount>,
|
|
41
65
|
) {
|
|
42
66
|
this.entities = entities
|
|
43
67
|
this.blockNumber = blockNumber
|
|
44
68
|
this.cursor = cursor
|
|
69
|
+
this.skipped = skipped
|
|
70
|
+
this.count = count
|
|
45
71
|
this.fetchPage = fetchPage
|
|
46
72
|
}
|
|
47
73
|
|
|
@@ -64,7 +90,7 @@ export class QueryResult<TEntity = Entity> {
|
|
|
64
90
|
*/
|
|
65
91
|
// Async so that "no more pages" arrives as a rejection like every other failure here, rather
|
|
66
92
|
// than as a synchronous throw the caller's `.catch` would miss.
|
|
67
|
-
async next(): Promise<QueryResult<TEntity>> {
|
|
93
|
+
async next(): Promise<QueryResult<TEntity, TCount>> {
|
|
68
94
|
if (this.cursor === undefined) {
|
|
69
95
|
throw new NoMoreResultsError()
|
|
70
96
|
}
|