@arkiv-network/sdk 0.6.7 → 0.6.8-dev.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +52 -16
  2. package/dist/chains/index.d.ts +25 -25
  3. package/dist/chains/index.d.ts.map +1 -1
  4. package/dist/{query-D45ecTcc.d.ts → entity-B3qWIdug.d.cts} +27 -27
  5. package/dist/entity-B3qWIdug.d.cts.map +1 -0
  6. package/dist/{query-nNQb9Y0j.d.cts → entity-BOra9GVW.d.ts} +27 -27
  7. package/dist/entity-BOra9GVW.d.ts.map +1 -0
  8. package/dist/{queryBuilder-DpD-ls8B.d.cts → index-Dushe05u.d.cts} +202 -74
  9. package/dist/index-Dushe05u.d.cts.map +1 -0
  10. package/dist/{queryBuilder-Ccq0PWJk.d.ts → index-t3wvdqvC.d.ts} +202 -74
  11. package/dist/index-t3wvdqvC.d.ts.map +1 -0
  12. package/dist/index.cjs +4 -9
  13. package/dist/index.d.cts +65 -3
  14. package/dist/index.d.cts.map +1 -1
  15. package/dist/index.d.ts +65 -3
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +3 -4
  18. package/dist/index.js.map +1 -1
  19. package/dist/query/index.cjs +5 -2
  20. package/dist/query/index.d.cts +3 -3
  21. package/dist/query/index.d.ts +3 -3
  22. package/dist/query/index.js +2 -2
  23. package/dist/{queryBuilder-DM-YO1Y7.cjs → queryBuilder-BlMPfusd.cjs} +204 -70
  24. package/dist/{queryBuilder-DonAg1aq.js → queryBuilder-D7iYS0vU.js} +188 -72
  25. package/dist/queryBuilder-D7iYS0vU.js.map +1 -0
  26. package/dist/types/index.d.cts +1 -1
  27. package/dist/types/index.d.ts +1 -1
  28. package/dist/utils/index.cjs +1 -8
  29. package/dist/utils/index.d.cts +0 -1
  30. package/dist/utils/index.d.cts.map +1 -1
  31. package/dist/utils/index.d.ts +0 -1
  32. package/dist/utils/index.d.ts.map +1 -1
  33. package/dist/utils/index.js +0 -2
  34. package/dist/utils/index.js.map +1 -1
  35. package/package.json +11 -15
  36. package/src/actions/public/buildQuery.ts +4 -0
  37. package/src/actions/public/getBlockTiming.ts +1 -1
  38. package/src/clients/createPublicClient.test.ts +2 -0
  39. package/src/clients/decorators/arkivPublic.ts +67 -1
  40. package/src/index.ts +0 -3
  41. package/src/query/engine.ts +22 -13
  42. package/src/query/index.ts +1 -0
  43. package/src/query/queryBuilder.ts +189 -94
  44. package/src/query/queryResult.ts +13 -7
  45. package/src/query/selectQueryBuilder.test.ts +210 -0
  46. package/src/query/selection.test.ts +120 -0
  47. package/src/query/selection.ts +116 -0
  48. package/src/types/rpcSchema.ts +1 -1
  49. package/src/utils/index.ts +0 -1
  50. package/dist/accounts/index.cjs +0 -9
  51. package/dist/accounts/index.d.cts +0 -1
  52. package/dist/accounts/index.d.ts +0 -1
  53. package/dist/accounts/index.js +0 -1
  54. package/dist/query-D45ecTcc.d.ts.map +0 -1
  55. package/dist/query-nNQb9Y0j.d.cts.map +0 -1
  56. package/dist/queryBuilder-Ccq0PWJk.d.ts.map +0 -1
  57. package/dist/queryBuilder-DonAg1aq.js.map +0 -1
  58. package/dist/queryBuilder-DpD-ls8B.d.cts.map +0 -1
  59. package/src/accounts/index.ts +0 -1
@@ -1,6 +1,10 @@
1
1
  import type { ArkivClient } from "../../clients/baseClient"
2
2
  import { QueryBuilder } from "../../query/queryBuilder"
3
3
 
4
+ /**
5
+ * @deprecated Use `client.select()` (see SelectQueryBuilder) instead. `buildQuery()` returns
6
+ * only the entity `key` unless data is explicitly opted in to, which is an easy mistake.
7
+ */
4
8
  export async function buildQuery(client: ArkivClient) {
5
9
  return new QueryBuilder(client)
6
10
  }
@@ -16,7 +16,7 @@ export async function getBlockTiming(client: ArkivClient) {
16
16
  })
17
17
  logger("Block timing %o", blockTiming)
18
18
  return {
19
- currentBlock: blockTiming.current_block,
19
+ currentBlock: BigInt(blockTiming.current_block),
20
20
  currentBlockTime: blockTiming.current_block_time,
21
21
  blockDuration: blockTiming.duration,
22
22
  }
@@ -20,6 +20,8 @@ test("creates", () => {
20
20
  expect(typeof client.getEntity).toBe("function")
21
21
  expect(client.buildQuery).toBeDefined()
22
22
  expect(typeof client.buildQuery).toBe("function")
23
+ expect(client.select).toBeDefined()
24
+ expect(typeof client.select).toBe("function")
23
25
  expect(client.query).toBeDefined()
24
26
  expect(typeof client.query).toBe("function")
25
27
  expect(client.getEntityCount).toBeDefined()
@@ -4,7 +4,8 @@ import { getEntity } from "../../actions/public/getEntity"
4
4
  import { getEntityCount } from "../../actions/public/getEntityCount"
5
5
  import { type QueryOptions, type QueryReturnType, query } from "../../actions/public/query"
6
6
  import { subscribeEntityEvents } from "../../actions/public/subscribeEntityEvents"
7
- import { QueryBuilder } from "../../query/queryBuilder"
7
+ import { QueryBuilder, SelectQueryBuilder } from "../../query/queryBuilder"
8
+ import type { EntitySelection, FullEntity, ProjectedEntity, SelectArg } from "../../query/selection"
8
9
  import type { Entity } from "../../types/entity"
9
10
  import type {
10
11
  OnEntityCreatedEvent,
@@ -55,12 +56,76 @@ export type PublicArkivActions<
55
56
  */
56
57
  getEntity: (key: Hex) => Promise<Entity>
57
58
 
59
+ /**
60
+ * Returns a SelectQueryBuilder for building and executing queries — the recommended way to
61
+ * read entities. You declare up front which parts of an entity you want returned, so results
62
+ * always contain exactly the data you asked for.
63
+ *
64
+ * - Docs: https://docs.arkiv.network/ts-sdk/actions/public/query
65
+ *
66
+ * @param selection - What to include in the results. Omit it (or pass `"*"`) to select everything,
67
+ * or pass an object to select specific parts (at least one field is required). Every part is
68
+ * opt-in, including the `key`. The selection is flat — each field maps to an entity field.
69
+ * {@link SelectArg}
70
+ * @returns A SelectQueryBuilder instance for building and executing queries. {@link SelectQueryBuilder}
71
+ *
72
+ * @example
73
+ * import { createPublicClient, http } from 'arkiv'
74
+ * import { braga } from 'arkiv/chains'
75
+ * import { eq } from 'arkiv/query'
76
+ *
77
+ * const client = createPublicClient({
78
+ * chain: braga,
79
+ * transport: http(),
80
+ * })
81
+ * // select everything
82
+ * await client.select().where(eq("category", "docs")).fetch()
83
+ * await client.select("*").where(eq("category", "docs")).fetch()
84
+ * // only the key
85
+ * await client.select({ key: true }).where(eq("category", "docs")).fetch()
86
+ * // select specific fields — result typed { owner: Hex; attributes: Attribute[] }
87
+ * await client.select({ owner: true, attributes: true }).fetch()
88
+ * // a single field — result typed { owner: Hex }
89
+ * await client.select({ owner: true }).fetch()
90
+ */
91
+ select: {
92
+ /** Select every field. Pass nothing or `"*"`; the returned entities contain all fields. */
93
+ (selection?: "*"): SelectQueryBuilder<FullEntity>
94
+ /**
95
+ * Pick the entity fields to return. Set the ones you want to `true` (at least one is required);
96
+ * the result is typed to exactly those fields, so reading anything else is a compile error.
97
+ *
98
+ * Available fields: `key`, `owner`, `creator`, `contentType`, `payload`, `attributes`,
99
+ * `expiresAtBlock`, `createdAtBlock`, `lastModifiedAtBlock`, `transactionIndexInBlock`,
100
+ * `operationIndexInTransaction`.
101
+ *
102
+ * Pass the selection inline so its fields stay literal `true`. A selection stored in a `let`/
103
+ * `const` variable widens to `boolean` and the result type can no longer be narrowed — annotate
104
+ * it `as const` (e.g. `const sel = { owner: true } as const`) in that case.
105
+ *
106
+ * @example
107
+ * client.select({ owner: true, attributes: true }) // entities typed { owner, attributes }
108
+ * client.select({ key: true, payload: true }) // includes payload → toText()/toJson() too
109
+ */
110
+ <const S extends EntitySelection>(selection: S): SelectQueryBuilder<ProjectedEntity<S>>
111
+ /**
112
+ * Dynamic selection: accepts a value typed {@link SelectArg} (e.g. built at runtime). The
113
+ * result cannot be narrowed in this case, so the entities are typed as the full entity.
114
+ */
115
+ (selection: SelectArg): SelectQueryBuilder<FullEntity>
116
+ }
117
+
58
118
  /**
59
119
  * Returns a QueryBuilder instance for building and executing queries.
60
120
  * The QueryBuilder object follows the Builder pattern, allowing you to chain methods to build a query and then execute it.
61
121
  *
62
122
  * - Docs: https://docs.arkiv.network/ts-sdk/actions/public/query
63
123
  *
124
+ * @deprecated Use {@link select} instead. `buildQuery()` returns only the entity `key` unless
125
+ * you remember to opt in to data with `withAttributes()`/`withMetadata()`/`withPayload()`, which
126
+ * is an easy mistake. `select()` makes the selection explicit. This method remains for backwards
127
+ * compatibility and will be removed in a future release.
128
+ *
64
129
  * @returns A QueryBuilder instance for building and executing queries. {@link QueryBuilder}
65
130
  *
66
131
  * @example
@@ -200,6 +265,7 @@ export function publicArkivActions<
200
265
  getEntity: (key: Hex) => getEntity(client, key),
201
266
  query: (rawQuery: string, queryOptions?: QueryOptions) => query(client, rawQuery, queryOptions),
202
267
  buildQuery: () => new QueryBuilder(client),
268
+ select: (selection?: SelectArg) => new SelectQueryBuilder(client, selection),
203
269
  getBlockTiming: () => getBlockTiming(client),
204
270
  getEntityCount: () => getEntityCount(client),
205
271
  subscribeEntityEvents: (
package/src/index.ts CHANGED
@@ -2,9 +2,6 @@
2
2
  * @module main
3
3
  */
4
4
 
5
- // Re-export all viem stuff
6
- export * from "viem"
7
-
8
5
  // export main arkiv stuff
9
6
  export type { ArkivClient } from "./clients/baseClient"
10
7
  export type { PublicArkivClient } from "./clients/createPublicClient"
@@ -55,6 +55,11 @@ export async function processQuery(
55
55
  withAttributes?: boolean | undefined
56
56
  withMetadata?: boolean | undefined
57
57
  withPayload?: boolean | undefined
58
+ /**
59
+ * Fully-resolved include-data for fine-grained selection. When provided it takes
60
+ * precedence over the `withAttributes`/`withMetadata`/`withPayload` booleans.
61
+ */
62
+ includeData?: RpcIncludeData | undefined
58
63
  },
59
64
  ) {
60
65
  const {
@@ -68,6 +73,7 @@ export async function processQuery(
68
73
  withAttributes,
69
74
  withMetadata,
70
75
  withPayload,
76
+ includeData,
71
77
  } = queryParams
72
78
 
73
79
  logger("Processing query with params %o", {
@@ -81,6 +87,7 @@ export async function processQuery(
81
87
  withAttributes,
82
88
  withMetadata,
83
89
  withPayload,
90
+ includeData,
84
91
  })
85
92
 
86
93
  let query = processPredicates(predicates)
@@ -98,19 +105,21 @@ export async function processQuery(
98
105
  }
99
106
 
100
107
  const queryOptions: RpcQueryOptions = {
101
- includeData: {
102
- key: true,
103
- attributes: withAttributes ?? false,
104
- payload: withPayload ?? false,
105
- contentType: withMetadata ?? false,
106
- expiration: withMetadata ?? false,
107
- owner: withMetadata ?? false,
108
- creator: withMetadata ?? false,
109
- createdAtBlock: withMetadata ?? false,
110
- lastModifiedAtBlock: withMetadata ?? false,
111
- transactionIndexInBlock: withMetadata ?? false,
112
- operationIndexInTransaction: withMetadata ?? false,
113
- } as RpcIncludeData,
108
+ includeData:
109
+ includeData ??
110
+ ({
111
+ key: true,
112
+ attributes: withAttributes ?? false,
113
+ payload: withPayload ?? false,
114
+ contentType: withMetadata ?? false,
115
+ expiration: withMetadata ?? false,
116
+ owner: withMetadata ?? false,
117
+ creator: withMetadata ?? false,
118
+ createdAtBlock: withMetadata ?? false,
119
+ lastModifiedAtBlock: withMetadata ?? false,
120
+ transactionIndexInBlock: withMetadata ?? false,
121
+ operationIndexInTransaction: withMetadata ?? false,
122
+ } as RpcIncludeData),
114
123
  }
115
124
 
116
125
  if (validAtBlock !== undefined) {
@@ -1,3 +1,4 @@
1
1
  export * from "./predicate"
2
2
  export * from "./queryBuilder"
3
3
  export * from "./queryResult"
4
+ export * from "./selection"
@@ -1,10 +1,12 @@
1
1
  import type { Hex } from "viem"
2
2
  import type { ArkivClient } from "../clients/baseClient"
3
- import type { RpcOrderByAttribute } from "../types/rpcSchema"
3
+ import type { Entity } from "../types/entity"
4
+ import type { RpcEntity, RpcIncludeData, RpcOrderByAttribute } from "../types/rpcSchema"
4
5
  import { entityFromRpcResult } from "../utils/entities"
5
6
  import { processQuery } from "./engine"
6
7
  import type { Predicate } from "./predicate"
7
8
  import { QueryResult } from "./queryResult"
9
+ import { type SelectArg, selectionToIncludeData } from "./selection"
8
10
 
9
11
  export type OrderByAttribute = {
10
12
  name: string
@@ -45,24 +47,37 @@ export function desc(attributeName: string, attributeType: "string" | "number"):
45
47
  order: "desc",
46
48
  }
47
49
  }
50
+
48
51
  /**
49
- * QueryBuilder is a helper class to build queries to the Arkiv DBChains.
50
- * It can be used to fetch entities from the Arkiv DBChains. It follows the Builder pattern allowing chaining of methods.
51
- * @param client - The Arkiv client
52
- * @returns The QueryBuilder instance {@link QueryBuilder}
52
+ * Data-selection parameters forwarded to the query engine on each fetch.
53
+ * Subclasses of {@link BaseQueryBuilder} decide which parts of an entity are fetched.
53
54
  */
54
- export class QueryBuilder {
55
- private _client: ArkivClient
56
- private _ownedBy: Hex | undefined
57
- private _createdBy: Hex | undefined
58
- private _orderBy: RpcOrderByAttribute[] | undefined
59
- private _validAtBlock: bigint | undefined
60
- private _withAttributes: boolean | undefined
61
- private _withMetadata: boolean | undefined
62
- private _withPayload: boolean | undefined
63
- private _limit: number | undefined
64
- private _cursor: string | undefined
65
- private _predicates: Predicate[]
55
+ type SelectionParams = {
56
+ withAttributes?: boolean | undefined
57
+ withMetadata?: boolean | undefined
58
+ withPayload?: boolean | undefined
59
+ includeData?: RpcIncludeData | undefined
60
+ }
61
+
62
+ /**
63
+ * BaseQueryBuilder holds the query-building logic shared by every query builder
64
+ * (filtering, ordering, pagination, execution). It follows the Builder pattern,
65
+ * allowing methods to be chained. Subclasses decide how data selection is expressed
66
+ * by implementing the protected `selectionParams` method.
67
+ *
68
+ * Use {@link SelectQueryBuilder} via `client.select()` to build and execute queries.
69
+ *
70
+ * @typeParam TEntity - The shape of each entity produced by {@link BaseQueryBuilder.fetch}.
71
+ */
72
+ export abstract class BaseQueryBuilder<TEntity> {
73
+ protected _client: ArkivClient
74
+ protected _ownedBy: Hex | undefined
75
+ protected _createdBy: Hex | undefined
76
+ protected _orderBy: RpcOrderByAttribute[] | undefined
77
+ protected _validAtBlock: bigint | undefined
78
+ protected _limit: number | undefined
79
+ protected _cursor: string | undefined
80
+ protected _predicates: Predicate[]
66
81
 
67
82
  constructor(client: ArkivClient) {
68
83
  this._client = client
@@ -72,13 +87,13 @@ export class QueryBuilder {
72
87
  /**
73
88
  * Sets the ownedBy filter
74
89
  * @param ownedBy - The address of the owner
75
- * @returns The QueryBuilder instance
90
+ * @returns The query builder instance
76
91
  *
77
92
  * @example
78
- * const builder = new QueryBuilder(client)
93
+ * const builder = client.select()
79
94
  * builder.ownedBy("0x1234567890123456789012345678901234567890")
80
95
  */
81
- ownedBy(ownedBy: Hex) {
96
+ ownedBy(ownedBy: Hex): this {
82
97
  this._ownedBy = ownedBy
83
98
  return this
84
99
  }
@@ -86,13 +101,13 @@ export class QueryBuilder {
86
101
  /**
87
102
  * Sets the createdBy filter
88
103
  * @param createdBy - The address of the creator
89
- * @returns The QueryBuilder instance
104
+ * @returns The query builder instance
90
105
  *
91
106
  * @example
92
- * const builder = new QueryBuilder(client)
107
+ * const builder = client.select()
93
108
  * builder.createdBy("0x1234567890123456789012345678901234567890")
94
109
  */
95
- createdBy(createdBy: Hex) {
110
+ createdBy(createdBy: Hex): this {
96
111
  this._createdBy = createdBy
97
112
  return this
98
113
  }
@@ -105,10 +120,10 @@ export class QueryBuilder {
105
120
  * @param attributeName - The name of the attribute to order by
106
121
  * @param attributeType - The type of the attribute to order by (string or number)
107
122
  * @param order - The order to set the order by (asc or desc)
108
- * @returns The QueryBuilder instance
123
+ * @returns The query builder instance
109
124
  *
110
125
  * @example
111
- * const builder = client.buildQuery()
126
+ * const builder = client.select()
112
127
  * builder.orderBy("name", "string", "desc")
113
128
  * builder.orderBy(asc("name", "string"))
114
129
  * builder.orderBy(desc("name", "string"))
@@ -119,10 +134,10 @@ export class QueryBuilder {
119
134
  * This method takes the OrderByAttribute object as an argument and is mainly
120
135
  * used to use the helper functions asc() and desc() to create the OrderByAttribute instances.
121
136
  * @param orderByAttribute - The OrderByAttribute instance to set
122
- * @returns The QueryBuilder instance
137
+ * @returns The query builder instance
123
138
  *
124
139
  * @example
125
- * const builder = new QueryBuilder(client)
140
+ * const builder = client.select()
126
141
  * builder.orderBy(asc("name", "string"))
127
142
  * builder.orderBy(desc("name", "string"))
128
143
  */
@@ -131,7 +146,7 @@ export class QueryBuilder {
131
146
  attributeNameOrOrderByAttribute: string | OrderByAttribute,
132
147
  attributeType?: "string" | "number",
133
148
  order: "asc" | "desc" = "asc",
134
- ) {
149
+ ): this {
135
150
  if (!this._orderBy) {
136
151
  this._orderBy = []
137
152
  }
@@ -160,58 +175,16 @@ export class QueryBuilder {
160
175
  return this
161
176
  }
162
177
 
163
- /**
164
- * Sets the withAttributes flag which will return the attributes for the entities if true
165
- * @param withAttributes - The boolean value to set
166
- * @returns The QueryBuilder instance
167
- *
168
- * @example
169
- * const builder = new QueryBuilder(client)
170
- * builder.withAttributes(true)
171
- */
172
- withAttributes(withAttributes: boolean = true) {
173
- this._withAttributes = withAttributes
174
- return this
175
- }
176
-
177
- /**
178
- * Sets the withMetadata flag which will return the metadata (like owner, expiredAt, etc.) for the entities if true
179
- * @param withMetadata - The boolean value to set
180
- * @returns The QueryBuilder instance
181
- *
182
- * @example
183
- * const builder = new QueryBuilder(client)
184
- * builder.withMetadata(true)
185
- */
186
- withMetadata(withMetadata: boolean = true) {
187
- this._withMetadata = withMetadata
188
- return this
189
- }
190
-
191
- /**
192
- * Sets the withPayload flag which will return the payload for the entities if true
193
- * @param withPayload - The boolean value to set
194
- * @returns The QueryBuilder instance
195
- *
196
- * @example
197
- * const builder = new QueryBuilder(client)
198
- * builder.withPayload(true)
199
- */
200
- withPayload(withPayload: boolean = true) {
201
- this._withPayload = withPayload
202
- return this
203
- }
204
-
205
178
  /**
206
179
  * Sets the limit for the query
207
180
  * @param limit - The number of entities to return
208
- * @returns The QueryBuilder instance
181
+ * @returns The query builder instance
209
182
  *
210
183
  * @example
211
- * const builder = new QueryBuilder(client)
184
+ * const builder = client.select()
212
185
  * builder.limit(10)
213
186
  */
214
- limit(limit: number) {
187
+ limit(limit: number): this {
215
188
  this._limit = limit
216
189
  return this
217
190
  }
@@ -219,13 +192,13 @@ export class QueryBuilder {
219
192
  /**
220
193
  * Sets the cursor for the query - it is advances setting which rather shouldn't be used manually but it is provided from query result if limit is used (pagination).
221
194
  * @param cursor - The cursor to set which tells to RPC Query server where to start or continue the query.
222
- * @returns The QueryBuilder instance
195
+ * @returns The query builder instance
223
196
  *
224
197
  * @example
225
- * const builder = new QueryBuilder(client)
226
- * builder.offset(10)
198
+ * const builder = client.select()
199
+ * builder.cursor("0xABC123")
227
200
  */
228
- cursor(cursor: string) {
201
+ cursor(cursor: string): this {
229
202
  this._cursor = cursor
230
203
  return this
231
204
  }
@@ -234,13 +207,13 @@ export class QueryBuilder {
234
207
  * Sets the validAtBlock for the query which tells at which block height the state we are intested.
235
208
  * If not set, the latest block is used.
236
209
  * @param validAtBlock - The block number to set
237
- * @returns The QueryBuilder instance
210
+ * @returns The query builder instance
238
211
  *
239
212
  * @example
240
- * const builder = new QueryBuilder(client)
213
+ * const builder = client.select()
241
214
  * builder.validAtBlock(10000)
242
215
  */
243
- validAtBlock(validAtBlock: bigint) {
216
+ validAtBlock(validAtBlock: bigint): this {
244
217
  this._validAtBlock = validAtBlock
245
218
  return this
246
219
  }
@@ -249,10 +222,10 @@ export class QueryBuilder {
249
222
  * Sets the predicates for the query limiting the results. It can be a single predicate or an array of predicates combined with 'and'.
250
223
  * Predicates can be nested using 'or' and 'and' predicates.
251
224
  * @param predicates - The predicates to set
252
- * @returns The QueryBuilder instance
225
+ * @returns The query builder instance
253
226
  *
254
227
  * @example
255
- * const builder = new QueryBuilder(client)
228
+ * const builder = client.select()
256
229
  * builder.where(eq("name", "John"))
257
230
  * builder.where([eq("name", "John"), eq("age", 30)])
258
231
  * builder.where([eq("name", "John"), or([eq("age", 30), eq("age", 31)])])
@@ -261,7 +234,7 @@ export class QueryBuilder {
261
234
  * builder.where([eq("name", "John"), and([eq("age", 30), or([eq("age", 31), eq("age", 32)])])])
262
235
  * builder.where([eq("name", "John"), and([eq("age", 30), or([eq("age", 31), and([eq("age", 32), eq("age", 33)])])])])
263
236
  */
264
- where(predicates: Predicate[] | Predicate) {
237
+ where(predicates: Predicate[] | Predicate): this {
265
238
  if (Array.isArray(predicates)) {
266
239
  this._predicates.push(...predicates)
267
240
  } else {
@@ -271,16 +244,28 @@ export class QueryBuilder {
271
244
  }
272
245
 
273
246
  /**
274
- * Fetches the entities from the query. Re
247
+ * Returns the data-selection parameters forwarded to the query engine.
248
+ * Subclasses decide which parts of an entity are fetched.
249
+ */
250
+ protected abstract selectionParams(): SelectionParams
251
+
252
+ /**
253
+ * Builds a single result entity from a raw RPC entity. Subclasses decide the produced shape
254
+ * (a full {@link Entity}, or a projected object inferred from the selection).
255
+ */
256
+ protected abstract projectEntity(rpcEntity: RpcEntity): TEntity | Promise<TEntity>
257
+
258
+ /**
259
+ * Fetches the entities from the query.
275
260
  * It will return a QueryResult instance which can be used to fetch the next and previous pages.
276
261
  * @returns The QueryResult instance {@link QueryResult}
277
262
  *
278
263
  * @example
279
- * const builder = new QueryBuilder(client)
264
+ * const builder = client.select()
280
265
  * const result = await builder.where(eq("name", "John")).fetch()
281
266
  * // result = { entities: [Entity, Entity, Entity], next: async () => QueryResult, previous: async () => QueryResult }
282
267
  */
283
- async fetch() {
268
+ async fetch(): Promise<QueryResult<TEntity>> {
284
269
  const queryResult = await processQuery(this._client, {
285
270
  predicates: this._predicates,
286
271
  limit: this._limit,
@@ -289,19 +274,15 @@ export class QueryBuilder {
289
274
  createdBy: this._createdBy,
290
275
  orderBy: this._orderBy,
291
276
  validAtBlock: this._validAtBlock,
292
- withAttributes: this._withAttributes,
293
- withMetadata: this._withMetadata,
294
- withPayload: this._withPayload,
277
+ ...this.selectionParams(),
295
278
  })
296
279
 
297
- const entities = await Promise.all(
298
- queryResult.data.map((entity) => entityFromRpcResult(entity)),
299
- )
280
+ const entities = await Promise.all(queryResult.data.map((entity) => this.projectEntity(entity)))
300
281
 
301
282
  this.cursor(queryResult.cursor)
302
283
  this.validAtBlock(BigInt(queryResult.blockNumber ?? 0))
303
284
 
304
- return new QueryResult(entities, this, this._cursor, this._limit, this._validAtBlock)
285
+ return new QueryResult<TEntity>(entities, this, this._cursor, this._limit, this._validAtBlock)
305
286
  }
306
287
 
307
288
  /**
@@ -309,7 +290,7 @@ export class QueryBuilder {
309
290
  * @returns The number of entities
310
291
  *
311
292
  * @example
312
- * const builder = new QueryBuilder(client)
293
+ * const builder = client.select()
313
294
  * const result = await builder.where(eq("name", "John")).count()
314
295
  * // result = 10
315
296
  */
@@ -330,3 +311,117 @@ export class QueryBuilder {
330
311
  return queryResult.data.length ?? 0
331
312
  }
332
313
  }
314
+
315
+ /**
316
+ * QueryBuilder is a helper class to build queries to the Arkiv DBChains.
317
+ * It can be used to fetch entities from the Arkiv DBChains. It follows the Builder pattern allowing chaining of methods.
318
+ *
319
+ * By default the result includes only the entity `key`. Additional data is opt-in through
320
+ * `withAttributes()`, `withMetadata()` and `withPayload()`.
321
+ *
322
+ * @deprecated Use {@link SelectQueryBuilder} via `client.select()` instead. Declaring the
323
+ * selection up front avoids the common mistake of forgetting to opt in to data and getting back
324
+ * entities with only their `key` populated. This class remains for backwards compatibility and
325
+ * will be removed in a future release.
326
+ *
327
+ * @param client - The Arkiv client
328
+ * @returns The QueryBuilder instance {@link QueryBuilder}
329
+ */
330
+ export class QueryBuilder extends BaseQueryBuilder<Entity> {
331
+ private _withAttributes: boolean | undefined
332
+ private _withMetadata: boolean | undefined
333
+ private _withPayload: boolean | undefined
334
+
335
+ /**
336
+ * Sets the withAttributes flag which will return the attributes for the entities if true
337
+ * @param withAttributes - The boolean value to set
338
+ * @returns The QueryBuilder instance
339
+ *
340
+ * @example
341
+ * const builder = client.buildQuery()
342
+ * builder.withAttributes(true)
343
+ */
344
+ withAttributes(withAttributes: boolean = true): this {
345
+ this._withAttributes = withAttributes
346
+ return this
347
+ }
348
+
349
+ /**
350
+ * Sets the withMetadata flag which will return the metadata (like owner, expiredAt, etc.) for the entities if true
351
+ * @param withMetadata - The boolean value to set
352
+ * @returns The QueryBuilder instance
353
+ *
354
+ * @example
355
+ * const builder = client.buildQuery()
356
+ * builder.withMetadata(true)
357
+ */
358
+ withMetadata(withMetadata: boolean = true): this {
359
+ this._withMetadata = withMetadata
360
+ return this
361
+ }
362
+
363
+ /**
364
+ * Sets the withPayload flag which will return the payload for the entities if true
365
+ * @param withPayload - The boolean value to set
366
+ * @returns The QueryBuilder instance
367
+ *
368
+ * @example
369
+ * const builder = client.buildQuery()
370
+ * builder.withPayload(true)
371
+ */
372
+ withPayload(withPayload: boolean = true): this {
373
+ this._withPayload = withPayload
374
+ return this
375
+ }
376
+
377
+ protected selectionParams(): SelectionParams {
378
+ return {
379
+ withAttributes: this._withAttributes,
380
+ withMetadata: this._withMetadata,
381
+ withPayload: this._withPayload,
382
+ }
383
+ }
384
+
385
+ protected projectEntity(rpcEntity: RpcEntity): Promise<Entity> {
386
+ return entityFromRpcResult(rpcEntity)
387
+ }
388
+ }
389
+
390
+ /**
391
+ * SelectQueryBuilder is the recommended query builder. It requires the selection to be declared
392
+ * up front, so results always contain exactly the data you asked for — and the type of each
393
+ * returned entity is narrowed to exactly the selected fields.
394
+ *
395
+ * The selection is fixed at construction time and is flat — each field maps to an entity field.
396
+ * Create one via `client.select(...)` rather than constructing it directly, so the result type is
397
+ * inferred from the selection.
398
+ *
399
+ * @typeParam TEntity - The projected entity shape, inferred from the selection by `client.select()`.
400
+ *
401
+ * @example
402
+ * // everything
403
+ * await client.select().where(eq("name", "John")).fetch()
404
+ * await client.select("*").where(eq("name", "John")).fetch()
405
+ * // specific fields
406
+ * await client.select({ owner: true, attributes: true }).fetch()
407
+ * // a single field — result is typed (flat) { owner: Hex }
408
+ * await client.select({ owner: true }).fetch()
409
+ */
410
+ export class SelectQueryBuilder<TEntity> extends BaseQueryBuilder<TEntity> {
411
+ private _includeData: RpcIncludeData
412
+
413
+ constructor(client: ArkivClient, selection?: SelectArg) {
414
+ super(client)
415
+ this._includeData = selectionToIncludeData(selection)
416
+ }
417
+
418
+ protected selectionParams(): SelectionParams {
419
+ return { includeData: this._includeData }
420
+ }
421
+
422
+ protected async projectEntity(rpcEntity: RpcEntity): Promise<TEntity> {
423
+ // The runtime value is a full Entity (with methods); the static type is narrowed to the
424
+ // selected fields via `client.select()`'s inference.
425
+ return (await entityFromRpcResult(rpcEntity)) as unknown as TEntity
426
+ }
427
+ }