@arkiv-network/sdk 0.6.8 → 0.7.0

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 (83) hide show
  1. package/README.md +52 -16
  2. package/dist/chains/index.d.cts +25 -25
  3. package/dist/consts-AUoL-v42.cjs +18 -0
  4. package/dist/consts-BV4PR-de.js +7 -0
  5. package/dist/consts-BV4PR-de.js.map +1 -0
  6. package/dist/{query-CO6f35AE.d.cts → entity-B6g_EPm1.d.cts} +78 -31
  7. package/dist/entity-B6g_EPm1.d.cts.map +1 -0
  8. package/dist/{query-BhdmAzYg.d.ts → entity-qoLMAuuk.d.ts} +78 -31
  9. package/dist/entity-qoLMAuuk.d.ts.map +1 -0
  10. package/dist/index-DdspxNAA.d.ts +481 -0
  11. package/dist/index-DdspxNAA.d.ts.map +1 -0
  12. package/dist/index-Ds9dwqb5.d.cts +481 -0
  13. package/dist/index-Ds9dwqb5.d.cts.map +1 -0
  14. package/dist/index.cjs +63 -28
  15. package/dist/index.d.cts +90 -5
  16. package/dist/index.d.cts.map +1 -1
  17. package/dist/index.d.ts +91 -6
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +58 -21
  20. package/dist/index.js.map +1 -1
  21. package/dist/{payload-Da9gJMG5.d.ts → payload-B2kFJyah.d.ts} +1 -1
  22. package/dist/{payload-Da9gJMG5.d.ts.map → payload-B2kFJyah.d.ts.map} +1 -1
  23. package/dist/payload-Claf2jUO.js +13 -0
  24. package/dist/payload-Claf2jUO.js.map +1 -0
  25. package/dist/{payload-vwU3Rc6P.cjs → payload-DpA5PA-w.cjs} +0 -17
  26. package/dist/query/index.cjs +10 -28
  27. package/dist/query/index.d.cts +3 -3
  28. package/dist/query/index.d.ts +3 -3
  29. package/dist/query/index.js +7 -28
  30. package/dist/query/index.js.map +1 -1
  31. package/dist/{queryBuilder-DonAg1aq.js → queryBuilder-D2uwP9jI.js} +209 -89
  32. package/dist/queryBuilder-D2uwP9jI.js.map +1 -0
  33. package/dist/{queryBuilder-DM-YO1Y7.cjs → queryBuilder-UW2MCaX4.cjs} +237 -87
  34. package/dist/types/index.d.cts +1 -1
  35. package/dist/types/index.d.ts +1 -1
  36. package/dist/utils/index.cjs +4 -10
  37. package/dist/utils/index.d.cts +0 -1
  38. package/dist/utils/index.d.cts.map +1 -1
  39. package/dist/utils/index.d.ts +1 -2
  40. package/dist/utils/index.d.ts.map +1 -1
  41. package/dist/utils/index.js +2 -3
  42. package/dist/utils/index.js.map +1 -1
  43. package/package.json +11 -15
  44. package/src/actions/public/buildQuery.ts +4 -0
  45. package/src/actions/public/query.ts +10 -0
  46. package/src/actions/wallet/createEntity.ts +12 -2
  47. package/src/actions/wallet/extendEntity.ts +6 -1
  48. package/src/actions/wallet/updateEntity.ts +12 -2
  49. package/src/clients/createPublicClient.test.ts +2 -0
  50. package/src/clients/decorators/arkivPublic.ts +67 -2
  51. package/src/clients/decorators/arkivWallet.ts +18 -0
  52. package/src/errors.ts +18 -0
  53. package/src/index.ts +0 -3
  54. package/src/query/engine.ts +22 -13
  55. package/src/query/index.ts +1 -0
  56. package/src/query/predicate.test.ts +43 -0
  57. package/src/query/predicate.ts +14 -10
  58. package/src/query/queryBuilder.test.ts +22 -0
  59. package/src/query/queryBuilder.ts +221 -106
  60. package/src/query/queryResult.ts +13 -7
  61. package/src/query/selectQueryBuilder.test.ts +210 -0
  62. package/src/query/selection.test.ts +120 -0
  63. package/src/query/selection.ts +116 -0
  64. package/src/types/attributes.ts +12 -0
  65. package/src/utils/arkivTransactions.test.ts +62 -0
  66. package/src/utils/arkivTransactions.ts +36 -27
  67. package/src/utils/index.ts +0 -1
  68. package/src/utils/validation.test.ts +60 -0
  69. package/src/utils/validation.ts +32 -0
  70. package/dist/accounts/index.cjs +0 -9
  71. package/dist/accounts/index.d.cts +0 -1
  72. package/dist/accounts/index.d.ts +0 -1
  73. package/dist/accounts/index.js +0 -1
  74. package/dist/payload-DKBYYjeA.js +0 -18
  75. package/dist/payload-DKBYYjeA.js.map +0 -1
  76. package/dist/query-BhdmAzYg.d.ts.map +0 -1
  77. package/dist/query-CO6f35AE.d.cts.map +0 -1
  78. package/dist/queryBuilder-BvoQXa_u.d.ts +0 -328
  79. package/dist/queryBuilder-BvoQXa_u.d.ts.map +0 -1
  80. package/dist/queryBuilder-DonAg1aq.js.map +0 -1
  81. package/dist/queryBuilder-zTsKXctT.d.cts +0 -328
  82. package/dist/queryBuilder-zTsKXctT.d.cts.map +0 -1
  83. package/src/accounts/index.ts +0 -1
@@ -9,6 +9,11 @@ export type QueryOptionsIncludeData = {
9
9
  payload?: boolean
10
10
  metadata?: boolean
11
11
  }
12
+ /**
13
+ * @deprecated Server-side ordering is not supported by the network. Sort the fetched
14
+ * entities in JavaScript instead (e.g. `entities.sort(...)`). This type will be removed
15
+ * in a future release.
16
+ */
12
17
  export type QueryOptionsOrderBy = {
13
18
  name: string
14
19
  type: "string" | "numeric"
@@ -18,6 +23,11 @@ export type QueryOptionsOrderBy = {
18
23
  export type QueryOptions = {
19
24
  includeData?: QueryOptionsIncludeData
20
25
  atBlock?: bigint
26
+ /**
27
+ * @deprecated Server-side ordering is not supported by the network, so this option has no
28
+ * effect on the returned order. Sort the fetched entities in JavaScript instead
29
+ * (e.g. `entities.sort(...)`). This option will be removed in a future release.
30
+ */
21
31
  orderBy?: QueryOptionsOrderBy[]
22
32
  resultsPerPage?: number | undefined
23
33
  cursor?: string | undefined
@@ -9,14 +9,24 @@ const logger = getLogger("actions:wallet:create-entity")
9
9
  /**
10
10
  * Parameters for the createEntity function.
11
11
  * - payload: The payload of the entity.
12
- * - attributes: The attributes of the entity.
12
+ * - attributes: The attributes of the entity. Attribute values may be strings
13
+ * or numbers, but numeric values **must be integers**. To store a non-integer,
14
+ * scale it to an integer (e.g. `1.5` -> `1500`) to preserve numeric ordering,
15
+ * or pass it as a string (e.g. `"1.5"`). A non-integer numeric value throws an
16
+ * {@link InvalidAttributeError}.
13
17
  * - contentType: The content type of the entity.
14
- * - expiresIn: The expires in of the entity in seconds.
18
+ * - expiresIn: How long until the entity expires, in seconds. Because Arkiv
19
+ * measures expiration in blocks (1 block = 2 seconds), this **must be a
20
+ * positive integer and a multiple of the block time (2 seconds)**
21
+ * Invalid values throw an {@link InvalidExpirationError}.
15
22
  */
16
23
  export type CreateEntityParameters = {
17
24
  payload: Uint8Array
25
+ /** Entity attributes. Numeric values must be integers (scale non-integers, e.g. `1.5` -> `1500`, or use a string). Throws {@link InvalidAttributeError} otherwise. */
18
26
  attributes: Attribute[]
19
27
  contentType: MimeType | string
28
+ /** Seconds until expiry. Must be a positive integer and a multiple of the 2s block time.
29
+ * Throws {@link InvalidExpirationError} otherwise. */
20
30
  expiresIn: number
21
31
  }
22
32
 
@@ -9,10 +9,15 @@ const logger = getLogger("actions:wallet:extend-entity")
9
9
  /**
10
10
  * Parameters for the extendEntity function.
11
11
  * - entityKey: The key of the entity to extend.
12
- * - expiresIn: The expires in of the entity in seconds.
12
+ * - expiresIn: How long to extend the entity by, in seconds. Because Arkiv
13
+ * measures expiration in blocks (1 block = 2 seconds), this **must be a
14
+ * positive integer and a multiple of the block time (2 seconds)**.
15
+ * Invalid values throw an {@link InvalidExpirationError}.
13
16
  */
14
17
  export type ExtendEntityParameters = {
15
18
  entityKey: Hex
19
+ /** Seconds to extend by. Must be a positive integer and a multiple of the 2s block time.
20
+ * Throws {@link InvalidExpirationError} otherwise. */
16
21
  expiresIn: number
17
22
  }
18
23
 
@@ -10,15 +10,25 @@ const logger = getLogger("actions:wallet:update-entity")
10
10
  * Parameters for the updateEntity function.
11
11
  * - entityKey: The key of the entity to update.
12
12
  * - payload: The payload of the entity.
13
- * - attributes: The attributes of the entity.
13
+ * - attributes: The attributes of the entity. Attribute values may be strings
14
+ * or numbers, but numeric values **must be integers**. To store a non-integer,
15
+ * scale it to an integer (e.g. `1.5` -> `1500`) to preserve numeric ordering,
16
+ * or pass it as a string (e.g. `"1.5"`). A non-integer numeric value throws an
17
+ * {@link InvalidAttributeError}.
14
18
  * - contentType: The content type of the entity.
15
- * - expiresIn: The expires in of the entity in seconds.
19
+ * - expiresIn: How long until the entity expires, in seconds. Because Arkiv
20
+ * measures expiration in blocks (1 block = 2 seconds), this **must be a
21
+ * positive integer and a multiple of the block time (2 seconds)**.
22
+ * Invalid values throw an {@link InvalidExpirationError}.
16
23
  */
17
24
  export type UpdateEntityParameters = {
18
25
  entityKey: Hex
19
26
  payload: Uint8Array
27
+ /** Entity attributes. Numeric values must be integers (scale non-integers, e.g. `1.5` -> `1500`, or use a string). Throws {@link InvalidAttributeError} otherwise. */
20
28
  attributes: Attribute[]
21
29
  contentType: MimeType | string
30
+ /** Seconds until expiry. Must be a positive integer and a multiple of the 2s block time.
31
+ * Throws {@link InvalidExpirationError} otherwise. */
22
32
  expiresIn: number
23
33
  }
24
34
 
@@ -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
@@ -100,7 +165,6 @@ export type PublicArkivActions<
100
165
  * payload: true,
101
166
  * metadata: true,
102
167
  * },
103
- * orderBy: [{ name: "key", type: "string", desc: "asc" }],
104
168
  * resultsPerPage: 10,
105
169
  * cursor: undefined,
106
170
  * atBlock: undefined,
@@ -200,6 +264,7 @@ export function publicArkivActions<
200
264
  getEntity: (key: Hex) => getEntity(client, key),
201
265
  query: (rawQuery: string, queryOptions?: QueryOptions) => query(client, rawQuery, queryOptions),
202
266
  buildQuery: () => new QueryBuilder(client),
267
+ select: (selection?: SelectArg) => new SelectQueryBuilder(client, selection),
203
268
  getBlockTiming: () => getBlockTiming(client),
204
269
  getEntityCount: () => getEntityCount(client),
205
270
  subscribeEntityEvents: (
@@ -56,6 +56,11 @@ export type WalletArkivActions<
56
56
  * @param txParams - Optional transaction parameters
57
57
  * @returns The created entity with transaction hash
58
58
  *
59
+ * @throws {InvalidExpirationError} If `expiresIn` is not a positive integer
60
+ * that is a multiple of the block time (2 seconds).
61
+ * @throws {InvalidAttributeError} If a numeric attribute value is not an
62
+ * integer.
63
+ *
59
64
  * @example
60
65
  * import { createPublicClient, http } from 'arkiv'
61
66
  * import { braga } from 'arkiv/chains'
@@ -91,6 +96,11 @@ export type WalletArkivActions<
91
96
  * @param txParams - Optional transaction parameters
92
97
  * @returns The updated entity with transaction hash
93
98
  *
99
+ * @throws {InvalidExpirationError} If `expiresIn` is not a positive integer
100
+ * that is a multiple of the block time (2 seconds).
101
+ * @throws {InvalidAttributeError} If a numeric attribute value is not an
102
+ * integer.
103
+ *
94
104
  * @example
95
105
  * import { createWalletClient, http } from 'arkiv'
96
106
  * import { braga } from 'arkiv/chains'
@@ -146,6 +156,9 @@ export type WalletArkivActions<
146
156
  * @param txParams - Optional transaction parameters
147
157
  * @returns The updated entity with transaction hash
148
158
  *
159
+ * @throws {InvalidExpirationError} If `expiresIn` is not a positive integer
160
+ * that is a multiple of the block time (2 seconds).
161
+ *
149
162
  * @example
150
163
  * import { createWalletClient, http } from 'arkiv'
151
164
  * import { braga } from 'arkiv/chains'
@@ -194,6 +207,11 @@ export type WalletArkivActions<
194
207
  * @param txParams - Optional transaction parameters
195
208
  * @returns The mutation result with transaction hash
196
209
  *
210
+ * @throws {InvalidExpirationError} If any create/update/extension `expiresIn`
211
+ * is not a positive integer that is a multiple of the block time (2 seconds).
212
+ * @throws {InvalidAttributeError} If a numeric attribute value is not an
213
+ * integer.
214
+ *
197
215
  * @example
198
216
  * import { createWalletClient, http } from 'arkiv'
199
217
  * import { braga } from 'arkiv/chains'
package/src/errors.ts CHANGED
@@ -1,9 +1,27 @@
1
+ import { BLOCK_TIME } from "./consts"
2
+
1
3
  export class EntityMutationError extends Error {
2
4
  constructor(message: string) {
3
5
  super(message)
4
6
  this.name = "EntityMutationError"
5
7
  }
6
8
  }
9
+ export class InvalidExpirationError extends Error {
10
+ constructor(expiresIn: unknown) {
11
+ super(
12
+ `Invalid expiresIn: ${String(expiresIn)}. expiresIn must be a positive integer and a multiple of the Arkiv block time (${BLOCK_TIME} seconds), because expiration is measured in whole blocks (1 block = ${BLOCK_TIME} seconds).`,
13
+ )
14
+ this.name = "InvalidExpirationError"
15
+ }
16
+ }
17
+ export class InvalidAttributeError extends Error {
18
+ constructor(key: string, value: number) {
19
+ super(
20
+ `Invalid numeric value for attribute "${key}": ${String(value)}. Numeric attribute values must be integers. To keep numeric ordering/range queries, scale it to an integer (e.g. Math.round(${String(value)} * 1000), dividing by 1000 on read); otherwise store it as a string (e.g. "${String(value)}").`,
21
+ )
22
+ this.name = "InvalidAttributeError"
23
+ }
24
+ }
7
25
 
8
26
  export class NoMoreResultsError extends Error {
9
27
  constructor() {
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"
@@ -244,6 +244,27 @@ describe("Predicate Factory Functions", () => {
244
244
  })
245
245
  })
246
246
 
247
+ test("creates or predicate from varargs", () => {
248
+ const result = or(eq("type", "admin"), eq("type", "moderator"))
249
+
250
+ expect(result).toEqual({
251
+ type: "or",
252
+ predicates: [
253
+ { type: "eq", key: "type", value: "admin" },
254
+ { type: "eq", key: "type", value: "moderator" },
255
+ ],
256
+ })
257
+ })
258
+
259
+ test("creates or predicate with no arguments", () => {
260
+ const result = or()
261
+
262
+ expect(result).toEqual({
263
+ type: "or",
264
+ predicates: [],
265
+ })
266
+ })
267
+
247
268
  test("creates or predicate with nested and", () => {
248
269
  const nestedAnd = and([eq("verified", 1), gte("age", 18)])
249
270
  const predicates = [eq("status", "premium"), nestedAnd]
@@ -314,6 +335,28 @@ describe("Predicate Factory Functions", () => {
314
335
  })
315
336
  })
316
337
 
338
+ test("creates and predicate from varargs", () => {
339
+ const result = and(eq("status", "active"), gte("age", 18), not("deleted"))
340
+
341
+ expect(result).toEqual({
342
+ type: "and",
343
+ predicates: [
344
+ { type: "eq", key: "status", value: "active" },
345
+ { type: "gte", key: "age", value: 18 },
346
+ { type: "not", key: "deleted", value: "" },
347
+ ],
348
+ })
349
+ })
350
+
351
+ test("creates and predicate with no arguments", () => {
352
+ const result = and()
353
+
354
+ expect(result).toEqual({
355
+ type: "and",
356
+ predicates: [],
357
+ })
358
+ })
359
+
317
360
  test("creates and predicate with nested or", () => {
318
361
  const nestedOr = or([eq("type", "admin"), eq("type", "moderator")])
319
362
  const predicates = [eq("active", 1), nestedOr]
@@ -14,32 +14,36 @@ export type Predicate =
14
14
 
15
15
  /**
16
16
  * Creates an OR predicate
17
- * @param predicates - The predicates to combine
17
+ * @param predicates - The predicates to combine, either as a single array or as separate arguments
18
18
  * @returns The OR predicate
19
19
  *
20
20
  * @example
21
- * const predicates = [eq("name", "John"), eq("age", 30)]
22
- * const result = or(predicates)
21
+ * const result = or(eq("name", "John"), eq("age", 30))
22
+ * // or equivalently: or([eq("name", "John"), eq("age", 30)])
23
23
  * // result = { type: "or", predicates: [eq("name", "John"), eq("age", 30)] }
24
24
  *
25
25
  */
26
- export function or(predicates: Predicate[]): Predicate {
27
- return { type: "or", predicates }
26
+ export function or(predicates: Predicate[]): Predicate
27
+ export function or(...predicates: Predicate[]): Predicate
28
+ export function or(...predicates: (Predicate | Predicate[])[]): Predicate {
29
+ return { type: "or", predicates: predicates.flat() }
28
30
  }
29
31
 
30
32
  /**
31
33
  * Creates an AND predicate
32
- * @param predicates - The predicates to combine
34
+ * @param predicates - The predicates to combine, either as a single array or as separate arguments
33
35
  * @returns The AND predicate
34
36
  *
35
37
  * @example
36
- * const predicates = [eq("name", "John"), eq("age", 30)]
37
- * const result = and(predicates)
38
+ * const result = and(eq("name", "John"), eq("age", 30))
39
+ * // or equivalently: and([eq("name", "John"), eq("age", 30)])
38
40
  * // result = { type: "and", predicates: [eq("name", "John"), eq("age", 30)] }
39
41
  *
40
42
  */
41
- export function and(predicates: Predicate[]): Predicate {
42
- return { type: "and", predicates }
43
+ export function and(predicates: Predicate[]): Predicate
44
+ export function and(...predicates: Predicate[]): Predicate
45
+ export function and(...predicates: (Predicate | Predicate[])[]): Predicate {
46
+ return { type: "and", predicates: predicates.flat() }
43
47
  }
44
48
 
45
49
  /**
@@ -78,6 +78,28 @@ describe("QueryBuilder", () => {
78
78
  })
79
79
  })
80
80
 
81
+ test("adds multiple predicates as varargs", async () => {
82
+ mockProcessQuery.mockResolvedValue({
83
+ data: [],
84
+ })
85
+
86
+ const builder = new QueryBuilder(mockClient)
87
+ await builder.where(eq("name", "test"), gt("age", 18), neq("status", "inactive")).fetch()
88
+
89
+ expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
90
+ predicates: [eq("name", "test"), gt("age", 18), neq("status", "inactive")],
91
+ limit: undefined,
92
+ offset: undefined,
93
+ ownedBy: undefined,
94
+ createdBy: undefined,
95
+ orderBy: undefined,
96
+ validBeforeBlock: undefined,
97
+ withAttributes: undefined,
98
+ withMetadata: undefined,
99
+ withPayload: undefined,
100
+ })
101
+ })
102
+
81
103
  test("chains multiple where() calls", async () => {
82
104
  mockProcessQuery.mockResolvedValue({
83
105
  data: [],