@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.
Files changed (184) hide show
  1. package/README.md +180 -61
  2. package/dist/chains/index.d.ts +17 -17
  3. package/dist/changeOwnership-Cy9gfyjN.d.cts +1160 -0
  4. package/dist/changeOwnership-Cy9gfyjN.d.cts.map +1 -0
  5. package/dist/changeOwnership-gXf-lBqI.d.ts +1160 -0
  6. package/dist/changeOwnership-gXf-lBqI.d.ts.map +1 -0
  7. package/dist/entity/index.cjs +4 -10
  8. package/dist/entity/index.d.cts +3 -3
  9. package/dist/entity/index.d.ts +3 -3
  10. package/dist/entity/index.js +3 -4
  11. package/dist/entity-BM1unrQf.cjs +41 -0
  12. package/dist/{entity-DJ0Gml9c.cjs → entity-BgaFJSaV.cjs} +4 -4
  13. package/dist/entity-CFAI1KyG.js +36 -0
  14. package/dist/entity-CFAI1KyG.js.map +1 -0
  15. package/dist/{entity-CniBgBSd.js → entity-CGqx_AU0.js} +4 -4
  16. package/dist/entity-CGqx_AU0.js.map +1 -0
  17. package/dist/{payload-AfUfSctb.d.ts → expirationTime-C7PZK49K.d.ts} +8 -37
  18. package/dist/expirationTime-C7PZK49K.d.ts.map +1 -0
  19. package/dist/expirationTime-CkIGNAg8.cjs +140 -0
  20. package/dist/{payload-D167BlJR.d.cts → expirationTime-DOB8V4B8.d.cts} +8 -37
  21. package/dist/expirationTime-DOB8V4B8.d.cts.map +1 -0
  22. package/dist/{expiry-Cgdb_dbq.js → expirationTime-UKPuA-LM.js} +62 -27
  23. package/dist/expirationTime-UKPuA-LM.js.map +1 -0
  24. package/dist/expiry-B48I-yTA.d.cts +40 -0
  25. package/dist/expiry-B48I-yTA.d.cts.map +1 -0
  26. package/dist/expiry-S2PUL8Hg.d.ts +40 -0
  27. package/dist/expiry-S2PUL8Hg.d.ts.map +1 -0
  28. package/dist/index-C-mMF5WZ.d.cts +735 -0
  29. package/dist/index-C-mMF5WZ.d.cts.map +1 -0
  30. package/dist/{index-BkqSnGJH.d.ts → index-C4zZqJ9S.d.ts} +5 -27
  31. package/dist/index-C4zZqJ9S.d.ts.map +1 -0
  32. package/dist/{index-CLhsFyVH.d.cts → index-CcVWYB6v.d.cts} +5 -27
  33. package/dist/index-CcVWYB6v.d.cts.map +1 -0
  34. package/dist/index-DCx2XC6S.d.ts +489 -0
  35. package/dist/index-DCx2XC6S.d.ts.map +1 -0
  36. package/dist/index-DLb7BfIw.d.ts +735 -0
  37. package/dist/index-DLb7BfIw.d.ts.map +1 -0
  38. package/dist/index-DzriOUm8.d.cts +489 -0
  39. package/dist/index-DzriOUm8.d.cts.map +1 -0
  40. package/dist/index.cjs +162 -146
  41. package/dist/index.d.cts +180 -133
  42. package/dist/index.d.cts.map +1 -1
  43. package/dist/index.d.ts +180 -133
  44. package/dist/index.d.ts.map +1 -1
  45. package/dist/index.js +93 -83
  46. package/dist/index.js.map +1 -1
  47. package/dist/params-BuIrJA9a.cjs +45 -0
  48. package/dist/params-DDJOnsNi.js +22 -0
  49. package/dist/params-DDJOnsNi.js.map +1 -0
  50. package/dist/query/index.cjs +9 -4
  51. package/dist/query/index.d.cts +6 -6
  52. package/dist/query/index.d.ts +6 -6
  53. package/dist/query/index.js +4 -5
  54. package/dist/query-D0KtuFh2.cjs +1625 -0
  55. package/dist/query-DpRDozmj.js +1417 -0
  56. package/dist/query-DpRDozmj.js.map +1 -0
  57. package/dist/schema/index.cjs +46 -0
  58. package/dist/schema/index.d.cts +2 -0
  59. package/dist/schema/index.d.ts +2 -0
  60. package/dist/schema/index.js +3 -0
  61. package/dist/{attr-BXTkpnoc.cjs → schema-B9AyXcgU.cjs} +513 -246
  62. package/dist/{attr-tmyREHtw.js → schema-DY4sqehl.js} +427 -244
  63. package/dist/schema-DY4sqehl.js.map +1 -0
  64. package/dist/types/index.cjs +1 -1
  65. package/dist/types/index.d.cts +5 -5
  66. package/dist/types/index.d.ts +5 -5
  67. package/dist/types/index.js +1 -1
  68. package/dist/utils/index.cjs +3 -5
  69. package/dist/utils/index.d.cts +3 -3
  70. package/dist/utils/index.d.ts +3 -3
  71. package/dist/utils/index.js +3 -3
  72. package/package.json +9 -12
  73. package/src/actions/public/getEntity.test.ts +69 -0
  74. package/src/actions/public/getEntity.ts +34 -20
  75. package/src/actions/public/predictEntityKeys.ts +8 -7
  76. package/src/actions/public/query.test.ts +58 -0
  77. package/src/actions/public/query.ts +41 -13
  78. package/src/actions/wallet/createEntity.ts +34 -63
  79. package/src/actions/wallet/executeBatch.test.ts +38 -0
  80. package/src/actions/wallet/executeBatch.ts +26 -9
  81. package/src/actions/wallet/extendEntity.ts +9 -8
  82. package/src/actions/wallet/patchEntity.ts +43 -52
  83. package/src/clients/createPublicClient.test.ts +10 -2
  84. package/src/clients/createWalletClient.ts +4 -1
  85. package/src/clients/decorators/arkivPublic.ts +123 -80
  86. package/src/clients/decorators/arkivWallet.ts +113 -65
  87. package/src/entity/create.test.ts +25 -21
  88. package/src/entity/expiry.ts +8 -6
  89. package/src/entity/flags.ts +1 -1
  90. package/src/entity/index.ts +2 -4
  91. package/src/entity/key.ts +3 -3
  92. package/src/entity/operations.ts +1 -1
  93. package/src/errors.ts +2 -11
  94. package/src/index.ts +18 -6
  95. package/src/query/builderBase.ts +166 -0
  96. package/src/query/countBuilder.test.ts +91 -0
  97. package/src/query/countBuilder.ts +67 -0
  98. package/src/query/engine.ts +9 -3
  99. package/src/query/errors.ts +24 -14
  100. package/src/query/expression.test.ts +129 -21
  101. package/src/query/expression.ts +323 -94
  102. package/src/query/index.ts +26 -4
  103. package/src/query/injection.test.ts +5 -11
  104. package/src/query/queryBuilder.test.ts +467 -58
  105. package/src/query/queryBuilder.ts +191 -98
  106. package/src/query/queryResult.ts +35 -9
  107. package/src/query/selection.test.ts +278 -24
  108. package/src/query/selection.ts +427 -86
  109. package/src/schema/attributes.test.ts +266 -0
  110. package/src/schema/attributes.ts +212 -0
  111. package/src/{attr → schema}/codec.test.ts +37 -5
  112. package/src/{attr → schema}/codec.ts +40 -11
  113. package/src/schema/column.ts +193 -0
  114. package/src/{attr → schema}/errors.ts +67 -10
  115. package/src/schema/index.ts +76 -0
  116. package/src/{attr → schema}/names.ts +4 -2
  117. package/src/schema/schema.test.ts +171 -0
  118. package/src/schema/schema.ts +250 -0
  119. package/src/{attr → schema}/types.ts +36 -30
  120. package/src/{attr → schema}/values.test.ts +62 -3
  121. package/src/{attr → schema}/values.ts +246 -65
  122. package/src/types/entity.test.ts +1 -44
  123. package/src/types/entity.ts +9 -55
  124. package/src/types/events.ts +1 -1
  125. package/src/types/index.ts +11 -4
  126. package/src/types/rpcSchema.ts +22 -16
  127. package/src/utils/arkivTransactions.test.ts +356 -116
  128. package/src/utils/arkivTransactions.ts +30 -38
  129. package/src/utils/entities.ts +10 -9
  130. package/src/utils/expirationTime.ts +5 -5
  131. package/src/utils/index.ts +0 -1
  132. package/src/utils/logger.test.ts +45 -0
  133. package/src/utils/logger.ts +5 -39
  134. package/src/utils/revert.test.ts +25 -0
  135. package/src/utils/revert.ts +28 -3
  136. package/dist/attr/index.cjs +0 -34
  137. package/dist/attr/index.d.cts +0 -2
  138. package/dist/attr/index.d.ts +0 -2
  139. package/dist/attr/index.js +0 -3
  140. package/dist/attr-tmyREHtw.js.map +0 -1
  141. package/dist/entity-B0t6sN7F.cjs +0 -71
  142. package/dist/entity-BIp_df9u.js +0 -67
  143. package/dist/entity-BIp_df9u.js.map +0 -1
  144. package/dist/entity-CniBgBSd.js.map +0 -1
  145. package/dist/expiry-BxOlvVWC.d.ts +0 -79
  146. package/dist/expiry-BxOlvVWC.d.ts.map +0 -1
  147. package/dist/expiry-CG9GhVXv.cjs +0 -135
  148. package/dist/expiry-Cgdb_dbq.js.map +0 -1
  149. package/dist/expiry-DRV29F1r.d.cts +0 -79
  150. package/dist/expiry-DRV29F1r.d.cts.map +0 -1
  151. package/dist/flags-DCD26uhe.cjs +0 -43
  152. package/dist/flags-DUT2gHEU.js +0 -33
  153. package/dist/flags-DUT2gHEU.js.map +0 -1
  154. package/dist/index-BkqSnGJH.d.ts.map +0 -1
  155. package/dist/index-C7fbakoI.d.cts +0 -278
  156. package/dist/index-C7fbakoI.d.cts.map +0 -1
  157. package/dist/index-CIcHPx2j.d.cts +0 -424
  158. package/dist/index-CIcHPx2j.d.cts.map +0 -1
  159. package/dist/index-CLhsFyVH.d.cts.map +0 -1
  160. package/dist/index-Cd2aaWme.d.ts +0 -424
  161. package/dist/index-Cd2aaWme.d.ts.map +0 -1
  162. package/dist/index-Cq6fa8AI.d.ts +0 -278
  163. package/dist/index-Cq6fa8AI.d.ts.map +0 -1
  164. package/dist/index-DaNeJUw8.d.ts +0 -955
  165. package/dist/index-DaNeJUw8.d.ts.map +0 -1
  166. package/dist/index-Dv16LO2s.d.cts +0 -955
  167. package/dist/index-Dv16LO2s.d.cts.map +0 -1
  168. package/dist/payload-AfUfSctb.d.ts.map +0 -1
  169. package/dist/payload-D167BlJR.d.cts.map +0 -1
  170. package/dist/payload-DE7wMztL.js +0 -92
  171. package/dist/payload-DE7wMztL.js.map +0 -1
  172. package/dist/payload-DqFweqiO.cjs +0 -108
  173. package/dist/query-9oojUdM7.cjs +0 -1128
  174. package/dist/query-Ber-lOJQ.js +0 -968
  175. package/dist/query-Ber-lOJQ.js.map +0 -1
  176. package/src/actions/public/getEntityCount.ts +0 -10
  177. package/src/attr/attributes.test.ts +0 -120
  178. package/src/attr/attributes.ts +0 -196
  179. package/src/attr/index.ts +0 -50
  180. package/src/types/mimeTypes.ts +0 -27
  181. package/src/utils/compression.test.ts +0 -32
  182. package/src/utils/compression.ts +0 -102
  183. package/src/utils/payload.ts +0 -36
  184. /package/src/{attr → schema}/decimal.ts +0 -0
@@ -7,11 +7,11 @@ import {
7
7
  stringToBytes,
8
8
  toHex,
9
9
  } from "viem"
10
+ import { type AnyColumn, type Column, createColumn, isColumn } from "./column"
10
11
  import { DECIMAL_UNIT, decimalToUnits, unitsToDecimal } from "./decimal"
11
12
  import { InvalidValueError, MissingValueError, UntypedValueError } from "./errors"
12
13
  import {
13
14
  type AddrValue,
14
- type AnyArkivValue,
15
15
  type ArkivValue,
16
16
  type BoolValue,
17
17
  type Bytes32Value,
@@ -22,6 +22,7 @@ import {
22
22
  type KeyValue,
23
23
  makeValue,
24
24
  type StrValue,
25
+ type TypeTag,
25
26
  type U64Value,
26
27
  type U256Value,
27
28
  } from "./types"
@@ -38,6 +39,11 @@ export const U256_MAX = 2n ** 256n - 1n
38
39
  * The longest `str`, in UTF-8 bytes
39
40
  */
40
41
  export const MAX_STRING_BYTES = 128
42
+ /**
43
+ * The longest `bytes` value, in bytes. The cap applies to each `bytes` attribute separately, but
44
+ * all the `bytes` values of one create or patch also share the `MAX_ENTITY_BYTES` budget.
45
+ */
46
+ export const MAX_BYTES_LENGTH = 128 * 1024
41
47
 
42
48
  /**
43
49
  * Finds the first C0 control or DEL — the characters a `str` may not contain.
@@ -50,25 +56,32 @@ function findControlChar(value: string): { index: number; code: number } | undef
50
56
  return undefined
51
57
  }
52
58
 
59
+ /** A `bool` column, indexed for equality once `.indexed()`. */
60
+ export function bool(): Column<"bool", false, false, false>
53
61
  /**
54
62
  * A boolean.
55
63
  *
56
- * `bool` is the one type that may also be written bare — `{ flagged: true }` and
57
- * `{ flagged: bool(true) }` are the same attribute.
64
+ * This is the default type for a bare boolean, so `{ flagged: true }` and
65
+ * `{ flagged: bool(true) }` are the same value.
58
66
  *
59
67
  * @example
60
68
  * bool(true) // { type: "bool", value: true }
61
69
  */
62
- export function bool(value: boolean): BoolValue {
70
+ export function bool(value: boolean): BoolValue
71
+ export function bool(...args: [] | [boolean]): BoolValue | Column<"bool", false, false, false> {
72
+ if (args.length === 0) return createColumn("bool", toTypedValue)
73
+ const [value] = args
63
74
  if (typeof value !== "boolean") {
64
75
  throw new InvalidValueError("bool", value, "not a boolean")
65
76
  }
66
77
  return makeValue("bool", value)
67
78
  }
68
79
 
80
+ /** An `i32` column, indexed for equality and range queries once `.indexed()`. */
81
+ export function i32(): Column<"i32", false, false, false>
69
82
  /**
70
- * A 32-bit signed integer, indexed for equality and range queries. This is the default type for a
71
- * bare number, so `{ level: 10 }` and `{ level: i32(10) }` are the same attribute.
83
+ * A 32-bit signed integer. This is the default type for a bare number, so `{ level: 10 }` and
84
+ * `{ level: i32(10) }` are the same value.
72
85
  *
73
86
  * @param value - An integer in [-2147483648, 2147483647].
74
87
  * @throws {InvalidValueError} If the value is not an integer, or does not fit an `i32`.
@@ -77,7 +90,12 @@ export function bool(value: boolean): BoolValue {
77
90
  * i32(10) // { type: "i32", value: 10 }
78
91
  * i32(-42) // negative values are fine — i32 is the signed integer
79
92
  */
80
- export function i32(value: number | bigint): I32Value {
93
+ export function i32(value: number | bigint): I32Value
94
+ export function i32(
95
+ ...args: [] | [number | bigint]
96
+ ): I32Value | Column<"i32", false, false, false> {
97
+ if (args.length === 0) return createColumn("i32", toTypedValue)
98
+ const [value] = args
81
99
  if (typeof value === "bigint") {
82
100
  if (value > BigInt(I32_MAX) || value < BigInt(I32_MIN)) {
83
101
  throw new InvalidValueError("i32", value, outOfI32Range(value >= 0n))
@@ -109,10 +127,11 @@ function outOfI32Range(nonNegative: boolean): string {
109
127
  : `${range} — use dec(...) for values that need more range than an i32`
110
128
  }
111
129
 
130
+ /** A `u256` column, indexed for equality and range queries once `.indexed()`. */
131
+ export function u256(): Column<"u256", false, false, false>
112
132
  /**
113
- * A 256-bit unsigned integer, indexed for equality and range queries. The type for token amounts
114
- * and anything else that can outgrow 2⁶⁴ — reach for {@link u64} first for counters, timestamps
115
- * and block heights.
133
+ * A 256-bit unsigned integer. The type for token amounts and anything else that can outgrow 2⁶⁴ —
134
+ * reach for {@link u64} first for counters, timestamps and block heights.
116
135
  *
117
136
  * @param value - A non-negative integer, as a `bigint`, a safe `number`, or a decimal or `0x` hex
118
137
  * string.
@@ -123,12 +142,18 @@ function outOfI32Range(nonNegative: boolean): string {
123
142
  * u256(Date.now()) // safe integers are accepted
124
143
  * u256("0xf4240") // hex, as it comes back over JSON-RPC
125
144
  */
126
- export function u256(value: bigint | number | string): U256Value {
127
- return makeValue("u256", toUnsigned("u256", value, U256_MAX))
145
+ export function u256(value: bigint | number | string): U256Value
146
+ export function u256(
147
+ ...args: [] | [bigint | number | string]
148
+ ): U256Value | Column<"u256", false, false, false> {
149
+ if (args.length === 0) return createColumn("u256", toTypedValue)
150
+ return makeValue("u256", toUnsigned("u256", args[0], U256_MAX))
128
151
  }
129
152
 
153
+ /** A `u64` column, indexed for equality and range queries once `.indexed()`. */
154
+ export function u64(): Column<"u64", false, false, false>
130
155
  /**
131
- * A 64-bit unsigned integer, indexed for equality and range queries.
156
+ * A 64-bit unsigned integer.
132
157
  *
133
158
  * This is the width the protocol counts in: block heights, `$expiresAt` and the entity-nonce are
134
159
  * all `u64`, so it is the type to reach for when a value has to line up with one of them. Use
@@ -143,8 +168,12 @@ export function u256(value: bigint | number | string): U256Value {
143
168
  * u64(Date.now()) // safe integers are accepted
144
169
  * u64("0x124f80") // hex, as it comes back over JSON-RPC
145
170
  */
146
- export function u64(value: bigint | number | string): U64Value {
147
- return makeValue("u64", toUnsigned("u64", value, U64_MAX))
171
+ export function u64(value: bigint | number | string): U64Value
172
+ export function u64(
173
+ ...args: [] | [bigint | number | string]
174
+ ): U64Value | Column<"u64", false, false, false> {
175
+ if (args.length === 0) return createColumn("u64", toTypedValue)
176
+ return makeValue("u64", toUnsigned("u64", args[0], U64_MAX))
148
177
  }
149
178
 
150
179
  /**
@@ -205,9 +234,10 @@ function toUnsignedBigint(tag: "u64" | "u256", value: bigint | number | string):
205
234
  throw new InvalidValueError(tag, value, "not a bigint, number or string")
206
235
  }
207
236
 
237
+ /** A `dec` column, indexed for equality and range queries once `.indexed()`. */
238
+ export function dec(): Column<"dec", false, false, false>
208
239
  /**
209
- * A signed fixed-point decimal with exactly 18 decimal places, indexed for equality and range
210
- * queries.
240
+ * A signed fixed-point decimal with exactly 18 decimal places.
211
241
  *
212
242
  * Pass fractional values as **strings**. A JavaScript number cannot represent most decimals
213
243
  * exactly, so accepting `dec(0.1 + 0.2)` would quietly store `0.30000000000000004`; requiring the
@@ -221,9 +251,14 @@ function toUnsignedBigint(tag: "u64" | "u256", value: bigint | number | string):
221
251
  * @example
222
252
  * dec("3.5")
223
253
  * dec("-0.000000000000000001") // the smallest step
224
- * dec(5) // integers may be passed bare
254
+ * dec(5) // integers may be passed as a number
225
255
  */
226
- export function dec(value: string | number | bigint): DecValue {
256
+ export function dec(value: string | number | bigint): DecValue
257
+ export function dec(
258
+ ...args: [] | [string | number | bigint]
259
+ ): DecValue | Column<"dec", false, false, false> {
260
+ if (args.length === 0) return createColumn("dec", toTypedValue)
261
+ const [value] = args
227
262
  if (typeof value === "string") {
228
263
  return makeValue("dec", unitsToDecimal(decimalToUnits(value)))
229
264
  }
@@ -266,8 +301,10 @@ export function decFromUnits(units: bigint): DecValue {
266
301
  return makeValue("dec", unitsToDecimal(units))
267
302
  }
268
303
 
304
+ /** A `str` column, indexed for equality and prefix (`STARTSWITH`) matching once `.indexed()`. */
305
+ export function str(): Column<"str", false, false, false>
269
306
  /**
270
- * A UTF-8 string of at most 128 bytes, indexed for equality and prefix (`STARTSWITH`) matching.
307
+ * A UTF-8 string of at most 128 bytes.
271
308
  *
272
309
  * The limit is on **bytes**, not characters: a string of 128 emoji does not fit.
273
310
  *
@@ -280,7 +317,10 @@ export function decFromUnits(units: bigint): DecValue {
280
317
  * @example
281
318
  * str("Bob")
282
319
  */
283
- export function str(value: string): StrValue {
320
+ export function str(value: string): StrValue
321
+ export function str(...args: [] | [string]): StrValue | Column<"str", false, false, false> {
322
+ if (args.length === 0) return createColumn("str", toTypedValue)
323
+ const [value] = args
284
324
  if (typeof value !== "string") {
285
325
  throw new InvalidValueError("str", value, "not a string")
286
326
  }
@@ -293,7 +333,7 @@ export function str(value: string): StrValue {
293
333
  `contains the control character U+${code} at index ${control.index}`,
294
334
  "A str is written into a query as str('...'), whose only escape is a doubled quote — there " +
295
335
  "is no spelling for a control character, so this value could be stored but never queried. " +
296
- "Free-form text belongs in the entity payload; keep a short, printable key here.",
336
+ "Free-form text belongs in a bytes column; keep a short, printable key here.",
297
337
  )
298
338
  }
299
339
  const bytes = stringToBytes(value).length
@@ -302,14 +342,16 @@ export function str(value: string): StrValue {
302
342
  "str",
303
343
  value,
304
344
  `${bytes} UTF-8 bytes exceeds the ${MAX_STRING_BYTES}-byte limit on a str`,
305
- "Store the long form in the entity payload and keep a short, queryable key here.",
345
+ "Store the long form in a bytes column and keep a short, queryable key here.",
306
346
  )
307
347
  }
308
348
  return makeValue("str", value)
309
349
  }
310
350
 
351
+ /** An `addr` column, indexed for equality once `.indexed()`. */
352
+ export function addr(): Column<"addr", false, false, false>
311
353
  /**
312
- * A 20-byte Ethereum address, indexed for equality.
354
+ * A 20-byte Ethereum address.
313
355
  *
314
356
  * All-lowercase and all-uppercase input carries no checksum and is accepted as-is; mixed-case
315
357
  * input is checked against its EIP-55 checksum, so a mistyped address is caught here rather than
@@ -320,7 +362,10 @@ export function str(value: string): StrValue {
320
362
  * @example
321
363
  * addr("0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045")
322
364
  */
323
- export function addr(value: string): AddrValue {
365
+ export function addr(value: string): AddrValue
366
+ export function addr(...args: [] | [string]): AddrValue | Column<"addr", false, false, false> {
367
+ if (args.length === 0) return createColumn("addr", toTypedValue)
368
+ const [value] = args
324
369
  if (typeof value !== "string" || !/^0x[0-9a-fA-F]{40}$/.test(value)) {
325
370
  throw new InvalidValueError("addr", value, "not a 20-byte 0x-prefixed hex address")
326
371
  }
@@ -338,8 +383,10 @@ export function addr(value: string): AddrValue {
338
383
  return makeValue("addr", checksumAddress(value as Address))
339
384
  }
340
385
 
386
+ /** A `key` column — a reference to another entity — indexed for equality once `.indexed()`. */
387
+ export function key(): Column<"key", false, false, false>
341
388
  /**
342
- * A reference to another entity, indexed for equality.
389
+ * A reference to another entity.
343
390
  *
344
391
  * References are **weak**: nothing checks that the target exists when you write it, and a
345
392
  * reference to a deleted or expired entity is allowed to dangle.
@@ -350,12 +397,18 @@ export function addr(value: string): AddrValue {
350
397
  * @example
351
398
  * key(parentEntityKey)
352
399
  */
353
- export function key(value: Hex | Uint8Array): KeyValue {
354
- return makeValue("key", asFixedHex("key", value, 32))
400
+ export function key(value: Hex | Uint8Array): KeyValue
401
+ export function key(
402
+ ...args: [] | [Hex | Uint8Array]
403
+ ): KeyValue | Column<"key", false, false, false> {
404
+ if (args.length === 0) return createColumn("key", toTypedValue)
405
+ return makeValue("key", asFixedHex("key", args[0], 32))
355
406
  }
356
407
 
408
+ /** A `bytes32` column, indexed for equality once `.indexed()`. */
409
+ export function bytes32(): Column<"bytes32", false, false, false>
357
410
  /**
358
- * 32 opaque bytes — a hash, a digest, an identifier from another system — indexed for equality.
411
+ * 32 opaque bytes — a hash, a digest, an identifier from another system.
359
412
  *
360
413
  * Use {@link key} instead when the bytes are an Arkiv entity key: same wire width, but the
361
414
  * distinct type makes the reference visible to readers and to the query language.
@@ -366,8 +419,50 @@ export function key(value: Hex | Uint8Array): KeyValue {
366
419
  * @example
367
420
  * bytes32(keccak256(toBytes("hello")))
368
421
  */
369
- export function bytes32(value: Hex | Uint8Array): Bytes32Value {
370
- return makeValue("bytes32", asFixedHex("bytes32", value, 32))
422
+ export function bytes32(value: Hex | Uint8Array): Bytes32Value
423
+ export function bytes32(
424
+ ...args: [] | [Hex | Uint8Array]
425
+ ): Bytes32Value | Column<"bytes32", false, false, false> {
426
+ if (args.length === 0) return createColumn("bytes32", toTypedValue)
427
+ return makeValue("bytes32", asFixedHex("bytes32", args[0], 32))
428
+ }
429
+
430
+ /**
431
+ * A `bytes` column — a blob. Bytes are stored but never indexed, so a bytes column has no
432
+ * `.indexed()`: it can be selected, not filtered on. A schema may have several, within the
433
+ * `MAX_ATTRIBUTES` cap on columns — but the `bytes` values of one create or patch share a single
434
+ * `MAX_ENTITY_BYTES` budget.
435
+ */
436
+ export function bytes(): Column<"bytes", false, false, false>
437
+ /**
438
+ * Opaque bytes of any length up to 128 KiB — a document, an image, a serialized object. This is the
439
+ * default type for a bare `Uint8Array`, so `{ body: buf }` and `{ body: bytes(buf) }` are the same
440
+ * value.
441
+ *
442
+ * @param value - The bytes, raw or as `0x` hex. Hex is taken as the bytes it spells, never as text.
443
+ * @throws {InvalidValueError} If the value is not bytes or hex, or exceeds 128 KiB.
444
+ *
445
+ * @example
446
+ * bytes(new TextEncoder().encode("# Hello"))
447
+ * bytes("0xdeadbeef")
448
+ */
449
+ export function bytes(value: Hex | Uint8Array): BytesValue
450
+ export function bytes(
451
+ ...args: [] | [Hex | Uint8Array]
452
+ ): BytesValue | Column<"bytes", false, false, false> {
453
+ if (args.length === 0) return createColumn("bytes", toTypedValue)
454
+ const [value] = args
455
+ const hex = asHex("bytes", value)
456
+ const length = (hex.length - 2) / 2
457
+ if (length > MAX_BYTES_LENGTH) {
458
+ throw new InvalidValueError(
459
+ "bytes",
460
+ `<${length} bytes>`,
461
+ `${length} bytes exceeds the ${MAX_BYTES_LENGTH}-byte limit on a bytes value`,
462
+ "Split it across several entities, or store the bulk elsewhere and keep a reference.",
463
+ )
464
+ }
465
+ return makeValue("bytes", hex)
371
466
  }
372
467
 
373
468
  /** Normalizes hex-or-bytes input of a known width into lowercase `0x` hex. */
@@ -378,39 +473,88 @@ function asFixedHex(tag: "key" | "bytes32", value: Hex | Uint8Array, size: numbe
378
473
  }
379
474
  return toHex(value)
380
475
  }
381
- if (typeof value !== "string" || !isHex(value, { strict: true })) {
382
- throw new InvalidValueError(tag, value, "not 0x-prefixed hex")
383
- }
384
- if (value.length !== 2 + size * 2) {
476
+ const hex = asHex(tag, value)
477
+ if (hex.length !== 2 + size * 2) {
385
478
  throw new InvalidValueError(
386
479
  tag,
387
480
  value,
388
- `${(value.length - 2) / 2} bytes, not exactly ${size} bytes`,
481
+ `${(hex.length - 2) / 2} bytes, not exactly ${size} bytes`,
389
482
  )
390
483
  }
484
+ return hex
485
+ }
486
+
487
+ /** Normalizes hex-or-bytes input into lowercase `0x` hex of a whole number of bytes. */
488
+ function asHex(tag: "key" | "bytes32" | "bytes", value: Hex | Uint8Array): Hex {
489
+ if (value instanceof Uint8Array) return toHex(value)
490
+ if (typeof value !== "string" || !isHex(value, { strict: true })) {
491
+ throw new InvalidValueError(tag, value, "not 0x-prefixed hex")
492
+ }
493
+ if (value.length % 2 !== 0) {
494
+ throw new InvalidValueError(tag, value, "an odd number of hex digits, so not whole bytes")
495
+ }
391
496
  return value.toLowerCase() as Hex
392
497
  }
393
498
 
394
499
  /**
395
500
  * A JavaScript value accepted wherever a typed value is expected: either a tagged value, or one of
396
- * the four bare forms that map unambiguously onto a single type.
501
+ * the five bare forms that map unambiguously onto a single type.
397
502
  *
398
- * | bare form | type |
399
- * | --------- | ------- |
400
- * | `boolean` | `bool` |
401
- * | `number` | `i32` |
402
- * | `bigint` | `u256` |
403
- * | `string` | `str` |
503
+ * | bare form | type |
504
+ * | ------------ | ------- |
505
+ * | `boolean` | `bool` |
506
+ * | `number` | `i32` |
507
+ * | `bigint` | `u256` |
508
+ * | `string` | `str` |
509
+ * | `Uint8Array` | `bytes` |
404
510
  *
405
511
  * Anything else — a large number, a decimal, an address, an entity key — must name its type, which
406
512
  * is what keeps `"0x1234..."` a string rather than silently becoming a key.
407
513
  *
408
514
  * These defaults apply identically when writing and when querying, so a value written bare and
409
- * queried bare always agrees with itself. The mismatch to watch for is mixing the two: an attribute
410
- * written as {@link u64} does not match a bare `bigint` predicate, because that predicate asserts
411
- * `u256`. A type mismatch is not an error to the engine — it simply matches nothing.
515
+ * queried bare always agrees with itself. The mismatch to watch for is a bare value where its
516
+ * column has another type: a bare `bigint` is a `u256`, not a {@link u64}. Through a schema that is
517
+ * caught — a write throws `SchemaMismatchError` and a query throws `InvalidPredicateError` — but a
518
+ * query without a schema sends the predicate as written, and the engine, for which a type mismatch
519
+ * is not an error, simply matches nothing.
520
+ */
521
+ export type ValueInput = ArkivValue | boolean | number | bigint | string | Uint8Array
522
+
523
+ /**
524
+ * The type a {@link ValueInput} resolves to — the tag of a tagged value, or the default of a bare
525
+ * form. `never` for anything else.
412
526
  */
413
- export type ValueInput = ArkivValue | boolean | number | bigint | string
527
+ export type TagOf<input> = input extends { readonly type: infer tag extends TypeTag }
528
+ ? tag
529
+ : input extends boolean
530
+ ? "bool"
531
+ : input extends number
532
+ ? "i32"
533
+ : input extends bigint
534
+ ? "u256"
535
+ : input extends string
536
+ ? "str"
537
+ : input extends Uint8Array
538
+ ? "bytes"
539
+ : never
540
+
541
+ /**
542
+ * The bare JavaScript form that defaults to `tag`, where there is one — `boolean` for `bool`,
543
+ * `number` for `i32`, `bigint` for `u256`, `string` for `str`, `Uint8Array` for `bytes`. The other
544
+ * types — `u64`, `dec`, `bytes32`, `addr` and `key` — must be written tagged.
545
+ */
546
+ export type BareOf<tag extends TypeTag> = {
547
+ bool: boolean
548
+ i32: number
549
+ u64: never
550
+ u256: bigint
551
+ dec: never
552
+ bytes32: never
553
+ bytes: Uint8Array
554
+ str: string
555
+ addr: never
556
+ key: never
557
+ }[tag]
414
558
 
415
559
  /**
416
560
  * Resolves a {@link ValueInput} to a typed value, applying the bare-form defaults above. Tagged
@@ -421,8 +565,8 @@ export type ValueInput = ArkivValue | boolean | number | bigint | string
421
565
  *
422
566
  * @throws {MissingValueError} If the input is `undefined` or `null` — an attribute is either set
423
567
  * with a value and a type, or not set at all.
424
- * @throws {UntypedValueError} If the input is not a tagged value or a bare boolean, number, bigint
425
- * or string.
568
+ * @throws {UntypedValueError} If the input is not a tagged value or a bare boolean, number, bigint,
569
+ * string or `Uint8Array`.
426
570
  * @throws {InvalidValueError} If a bare value does not fit the type it defaults to.
427
571
  *
428
572
  * @example
@@ -449,26 +593,61 @@ export function toValue(input: ValueInput, attributeName?: string): ArkivValue {
449
593
  case "string":
450
594
  return str(input)
451
595
  default: {
452
- // Widened to AnyArkivValue: `bytes` is not a member of ValueInput, but it is what a decoded
453
- // `$payload` carries, so a round-trip through a decoded entity must be rejected by name
454
- // rather than fall through to "cannot infer a type".
455
- const candidate = input as AnyArkivValue
456
- if (isArkivValue(candidate)) {
457
- if (candidate.type === "bytes") {
458
- throw new InvalidValueError(
459
- "bytes",
460
- candidate.value,
461
- "system-only — bytes backs the entity payload and cannot be set as an attribute",
462
- "Pass the bytes as the entity payload instead.",
463
- )
464
- }
465
- return revalidate(candidate)
466
- }
596
+ if (input instanceof Uint8Array) return bytes(input)
597
+ if (isArkivValue(input)) return revalidate(input)
598
+ // Widened: a column is not a ValueInput, but it is the one mix-up the dual-mode constructors
599
+ // invite — `str()` where `str("...")` was meant — so it is named rather than left untyped.
600
+ const candidate: unknown = input
601
+ if (isColumn(candidate)) throw columnGivenAsValue(candidate)
467
602
  throw new UntypedValueError(input)
468
603
  }
469
604
  }
470
605
  }
471
606
 
607
+ /**
608
+ * Resolves `input` to a value of type `tag` — a tagged value of that type, or whatever `tag`'s
609
+ * constructor accepts. What a column's `.default(...)` runs its argument through.
610
+ *
611
+ * @throws {InvalidValueError} If the input does not make a valid value of type `tag`.
612
+ */
613
+ function toTypedValue(tag: TypeTag, input: unknown): ArkivValue {
614
+ if (isArkivValue(input)) {
615
+ const value = revalidate(input)
616
+ if (value.type !== tag) {
617
+ throw new InvalidValueError(tag, input, `a ${value.type} value, not a ${tag}`)
618
+ }
619
+ return value
620
+ }
621
+ if (isColumn(input)) throw columnGivenAsValue(input)
622
+ // Through the constructor, so every type accepts exactly what its constructor does.
623
+ const construct = CONSTRUCTORS[tag] as (value: unknown) => ArkivValue
624
+ return construct(input)
625
+ }
626
+
627
+ function columnGivenAsValue(column: AnyColumn): InvalidValueError {
628
+ return new InvalidValueError(
629
+ column.type,
630
+ `<${column.type} column>`,
631
+ "a column, not a value",
632
+ `Pass the value itself, e.g. ${column.type}(...) with an argument — ${column.type}() with ` +
633
+ "none declares a schema column.",
634
+ )
635
+ }
636
+
637
+ /** Each tag's constructor, called with a value. */
638
+ const CONSTRUCTORS: { readonly [tag in TypeTag]: (value: never) => ArkivValue } = {
639
+ bool,
640
+ i32,
641
+ u64,
642
+ u256,
643
+ dec,
644
+ bytes32,
645
+ bytes,
646
+ str,
647
+ addr,
648
+ key,
649
+ }
650
+
472
651
  /**
473
652
  * Re-runs a tagged value through its own constructor.
474
653
  *
@@ -482,7 +661,7 @@ export function toValue(input: ValueInput, attributeName?: string): ArkivValue {
482
661
  * has been range-, length- and format-checked" — true at runtime. For a value that really did come
483
662
  * from a constructor it is a no-op that returns an equal value.
484
663
  */
485
- function revalidate(value: Exclude<AnyArkivValue, BytesValue>): ArkivValue {
664
+ function revalidate(value: ArkivValue): ArkivValue {
486
665
  switch (value.type) {
487
666
  case "bool":
488
667
  return bool(value.value)
@@ -496,6 +675,8 @@ function revalidate(value: Exclude<AnyArkivValue, BytesValue>): ArkivValue {
496
675
  return dec(value.value)
497
676
  case "bytes32":
498
677
  return bytes32(value.value)
678
+ case "bytes":
679
+ return bytes(value.value)
499
680
  case "str":
500
681
  return str(value.value)
501
682
  case "addr":
@@ -1,13 +1,9 @@
1
1
  import { describe, expect, it } from "bun:test"
2
- import { i32, str } from "../attr"
2
+ import { i32, str } from "../schema"
3
3
  import { Entity } from "./entity"
4
4
 
5
5
  const KEY = `0x${"ab".repeat(32)}` as const
6
6
 
7
- function withPayload(text: string): Entity {
8
- return new Entity({ key: KEY, payload: new TextEncoder().encode(text) })
9
- }
10
-
11
7
  describe("Entity", () => {
12
8
  it("carries only the fields it was built with", () => {
13
9
  const entity = new Entity({
@@ -20,7 +16,6 @@ describe("Entity", () => {
20
16
  expect(entity.attributes?.level.value).toBe(10)
21
17
  // Not selected is not the same as empty: nothing here stands in for data that was not asked for.
22
18
  expect(entity.owner).toBeUndefined()
23
- expect(entity.payload).toBeUndefined()
24
19
  expect(entity.attributeSchema).toBeUndefined()
25
20
  })
26
21
 
@@ -28,41 +23,3 @@ describe("Entity", () => {
28
23
  expect(new Entity().key).toBeUndefined()
29
24
  })
30
25
  })
31
-
32
- describe("toText", () => {
33
- it("decodes the payload as UTF-8", () => {
34
- expect(withPayload("hello world").toText()).toBe("hello world")
35
- expect(withPayload("").toText()).toBe("")
36
- })
37
-
38
- it("says how to ask for the payload when it was not selected", () => {
39
- expect(() => new Entity({ key: KEY }).toText()).toThrow(/select\(\{ payload: true \}\)/)
40
- })
41
- })
42
-
43
- describe("toJson", () => {
44
- it("parses objects and arrays", () => {
45
- expect(withPayload('{"a":1,"b":[2,3]}').toJson()).toEqual({ a: 1, b: [2, 3] })
46
- expect(withPayload("[1,2,3]").toJson()).toEqual([1, 2, 3])
47
- })
48
-
49
- it("throws on an empty payload rather than returning undefined", () => {
50
- expect(() => withPayload("").toJson()).toThrow(/empty payload/)
51
- })
52
-
53
- it("keeps the parse failure as the cause", () => {
54
- let caught: unknown
55
- try {
56
- withPayload("not json").toJson()
57
- } catch (error) {
58
- caught = error
59
- }
60
- expect(caught).toBeInstanceOf(Error)
61
- expect((caught as Error).message).toMatch(/Failed to parse entity payload as JSON/)
62
- expect((caught as Error).cause).toBeInstanceOf(Error)
63
- })
64
-
65
- it("reports the missing payload, not a JSON error", () => {
66
- expect(() => new Entity({ key: KEY }).toJson()).toThrow(/did not select it/)
67
- })
68
- })