@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
@@ -0,0 +1,193 @@
1
+ import type { Hex } from "viem"
2
+ import { SchemaDefinitionError } from "./errors"
3
+ import type { ArkivValue, IndexableTypeTag, TypeTag, ValueOf } from "./types"
4
+
5
+ /**
6
+ * What each type's constructor accepts as a value — `dec("4.1")`, `addr("0x…")`, `bytes(buf)`.
7
+ * A column's `.default(...)` takes the same thing, or an already-typed value.
8
+ */
9
+ export type ValueArgument<T extends TypeTag> = {
10
+ bool: boolean
11
+ i32: number | bigint
12
+ u64: bigint | number | string
13
+ u256: bigint | number | string
14
+ dec: string | number | bigint
15
+ bytes32: Hex | Uint8Array
16
+ bytes: Hex | Uint8Array
17
+ str: string
18
+ addr: string
19
+ key: Hex | Uint8Array
20
+ }[T]
21
+
22
+ /**
23
+ * A column: the declaration of one attribute in a schema — its type, whether the node indexes it,
24
+ * whether an entity may leave it unset, and the value the SDK writes when a create leaves it out.
25
+ *
26
+ * Built by calling a type constructor with no arguments — `str()`, `dec()`, `bytes()` — and refined
27
+ * by chaining. Every refinement returns a new column; a column is never modified in place.
28
+ *
29
+ * @typeParam T - The attribute's type tag.
30
+ * @typeParam I - Whether the attribute is indexed.
31
+ * @typeParam O - Whether the attribute may be absent.
32
+ * @typeParam D - Whether the column has a default.
33
+ *
34
+ * @example
35
+ * str() // stored, not indexed, required
36
+ * str().indexed() // filterable with eq / ne / startsWith
37
+ * dec().indexed().optional() // filterable, and an entity may leave it unset
38
+ * str().default("text/markdown") // filled in on create when left out
39
+ * bytes() // a blob; bytes has no .indexed()
40
+ */
41
+ export type Column<
42
+ T extends TypeTag = TypeTag,
43
+ I extends boolean = boolean,
44
+ O extends boolean = boolean,
45
+ D extends boolean = boolean,
46
+ > = ColumnProperties<T, I, O, D> & (T extends IndexableTypeTag ? IndexableColumn<T, O, D> : unknown)
47
+
48
+ /**
49
+ * Any column, whatever its type and flags — the shape a schema is built from. Written out rather
50
+ * than derived from {@link Column}, whose refinement methods would make every column's type
51
+ * invariant in its tag.
52
+ */
53
+ export type AnyColumn = {
54
+ readonly type: TypeTag
55
+ readonly isIndexed: boolean
56
+ readonly isOptional: boolean
57
+ readonly defaultValue: ArkivValue | undefined
58
+ }
59
+
60
+ type ColumnProperties<
61
+ T extends TypeTag,
62
+ I extends boolean,
63
+ O extends boolean,
64
+ D extends boolean,
65
+ > = {
66
+ /** The attribute's type tag. */
67
+ readonly type: T
68
+ /** Whether the node indexes the attribute, which is what makes it usable in `where`. */
69
+ readonly isIndexed: I
70
+ /**
71
+ * Whether an entity may leave the attribute unset. A create may then omit it, a patch may unset
72
+ * it, and a read gives back `undefined` where it is missing.
73
+ */
74
+ readonly isOptional: O
75
+ /** The value a create writes when the attribute is left out, if the column has one. */
76
+ readonly defaultValue: D extends true ? ValueOf<T> : undefined
77
+
78
+ /** The same column, allowed to be absent. */
79
+ optional(): Column<T, I, true, D>
80
+
81
+ /**
82
+ * The same column, with a value that a create writes whenever the attribute is left out. The
83
+ * default is applied by the SDK, not the node, and only on create.
84
+ *
85
+ * @param value - What the type's constructor would accept — `dec().default("4.1")` — or a value
86
+ * of the column's type.
87
+ * @throws {InvalidValueError} If the value does not fit the column's type.
88
+ */
89
+ default(value: ValueArgument<T> | ValueOf<T>): Column<T, I, O, true>
90
+ }
91
+
92
+ type IndexableColumn<T extends IndexableTypeTag, O extends boolean, D extends boolean> = {
93
+ /**
94
+ * The same column, indexed by the node. Only indexed attributes can be filtered on in `where`;
95
+ * an unindexed one can be stored and selected, and does not pay for an index.
96
+ */
97
+ indexed(): Column<T, true, O, D>
98
+ }
99
+
100
+ /**
101
+ * Marks a column. A registry symbol rather than `instanceof`, so a column made by one copy of the
102
+ * SDK — its ESM build, say — is still recognised by another, such as the CJS build a dependency
103
+ * loaded.
104
+ */
105
+ const COLUMN_BRAND = Symbol.for("@arkiv-network/sdk/column")
106
+
107
+ /** Resolves the input to `.default(...)` into a value of the column's type. */
108
+ type Coerce = (tag: TypeTag, input: unknown) => ArkivValue
109
+
110
+ class ColumnDefinition {
111
+ readonly type: TypeTag
112
+ readonly isIndexed: boolean
113
+ readonly isOptional: boolean
114
+ readonly defaultValue: ArkivValue | undefined
115
+ readonly #coerce: Coerce
116
+
117
+ constructor(
118
+ type: TypeTag,
119
+ isIndexed: boolean,
120
+ isOptional: boolean,
121
+ defaultValue: ArkivValue | undefined,
122
+ coerce: Coerce,
123
+ ) {
124
+ this.type = type
125
+ this.isIndexed = isIndexed
126
+ this.isOptional = isOptional
127
+ this.defaultValue = defaultValue
128
+ this.#coerce = coerce
129
+ Object.defineProperty(this, COLUMN_BRAND, { value: true })
130
+ Object.freeze(this)
131
+ }
132
+
133
+ indexed(): ColumnDefinition {
134
+ // Unreachable from typed code — a bytes column has no `indexed` — so this is for the callers
135
+ // the type system does not cover.
136
+ if (this.type === "bytes") {
137
+ throw new SchemaDefinitionError(
138
+ "a bytes column cannot be indexed. Bytes are stored, never indexed, so they can be " +
139
+ "selected but not filtered on — index a str, bytes32 or key derived from them instead",
140
+ )
141
+ }
142
+ return this.#with({ isIndexed: true })
143
+ }
144
+
145
+ optional(): ColumnDefinition {
146
+ return this.#with({ isOptional: true })
147
+ }
148
+
149
+ default(...args: unknown[]): ColumnDefinition {
150
+ // Counted rather than compared to undefined: `.default(undefined)` is a mistake to report, not
151
+ // a request for no default.
152
+ if (args.length === 0) {
153
+ throw new SchemaDefinitionError(
154
+ `.default() needs a value — e.g. ${this.type}().default(...). Leave .default() off for a ` +
155
+ "column the create must always provide",
156
+ )
157
+ }
158
+ return this.#with({ defaultValue: this.#coerce(this.type, args[0]) })
159
+ }
160
+
161
+ #with(
162
+ changes: Partial<Pick<ColumnDefinition, "isIndexed" | "isOptional" | "defaultValue">>,
163
+ ): ColumnDefinition {
164
+ return new ColumnDefinition(
165
+ this.type,
166
+ changes.isIndexed ?? this.isIndexed,
167
+ changes.isOptional ?? this.isOptional,
168
+ changes.defaultValue ?? this.defaultValue,
169
+ this.#coerce,
170
+ )
171
+ }
172
+ }
173
+
174
+ /**
175
+ * A fresh column of type `tag`: unindexed, required, no default. Internal seam for the type
176
+ * constructors, which return one when called with no arguments.
177
+ */
178
+ export function createColumn<T extends TypeTag>(
179
+ tag: T,
180
+ coerce: Coerce,
181
+ ): Column<T, false, false, false> {
182
+ return new ColumnDefinition(tag, false, false, undefined, coerce) as unknown as Column<
183
+ T,
184
+ false,
185
+ false,
186
+ false
187
+ >
188
+ }
189
+
190
+ /** Whether `input` is a column, as opposed to a value or anything else. */
191
+ export function isColumn(input: unknown): input is AnyColumn {
192
+ return typeof input === "object" && input !== null && COLUMN_BRAND in input
193
+ }
@@ -23,8 +23,8 @@ export class InvalidValueError extends Error {
23
23
 
24
24
  /**
25
25
  * A bare JavaScript value could not be given a type. Raised when an attribute or predicate is
26
- * handed something that is neither a tagged value nor one of the four bare forms
27
- * (`boolean`, `number`, `bigint`, `string`).
26
+ * handed something that is neither a tagged value nor one of the five bare forms
27
+ * (`boolean`, `number`, `bigint`, `string`, `Uint8Array`).
28
28
  */
29
29
  export class UntypedValueError extends Error {
30
30
  readonly input: unknown
@@ -33,7 +33,7 @@ export class UntypedValueError extends Error {
33
33
  super(
34
34
  `Cannot infer an Arkiv type for ${describe(input)}. ` +
35
35
  "Attribute values must be a tagged value (i32, u64, u256, dec, str, addr, key, bytes32, " +
36
- "bool) or a bare boolean, number, bigint or string.",
36
+ "bytes, bool) or a bare boolean, number, bigint, string or Uint8Array.",
37
37
  )
38
38
  this.name = "UntypedValueError"
39
39
  this.input = input
@@ -44,8 +44,12 @@ export class UntypedValueError extends Error {
44
44
  * An attribute was given `undefined` or `null` as its value.
45
45
  *
46
46
  * There is no such thing as an attribute with no value: an attribute either exists with a type, or
47
- * is not set. The fix is always to leave the name out of the map rather than to write an empty
48
- * value, so this is called out separately from {@link UntypedValueError} — the remedy is different.
47
+ * is not set. The fix is always to leave the name out rather than to write an empty value — or, to
48
+ * remove a column from an existing entity, to `unset` it in a patch — so this is called out
49
+ * separately from {@link UntypedValueError}: the remedy is different.
50
+ *
51
+ * `createEntity` and `patchEntity` already treat a column given as `undefined` as left out, so
52
+ * through them only `null` raises this.
49
53
  */
50
54
  export class MissingValueError extends Error {
51
55
  /** The attribute whose value was missing, when raised from an attribute map. */
@@ -57,8 +61,9 @@ export class MissingValueError extends Error {
57
61
  `The ${subject} is ${input === null ? "null" : "undefined"}. An attribute is either set ` +
58
62
  "with a value and a type or not set at all, so there is nothing to write here. " +
59
63
  (attributeName
60
- ? `Omit "${attributeName}" from the attributes instead — e.g. build the map ` +
61
- "conditionally, or drop the key when the value is absent."
64
+ ? `Leave "${attributeName}" out of the values (or a patch's "set") instead — e.g. ` +
65
+ "build the object conditionally, or drop the key when the value is absent. To remove " +
66
+ `the column from an existing entity, list it in a patch's "unset".`
62
67
  : "Omit the attribute instead of giving it an empty value."),
63
68
  )
64
69
  this.name = "MissingValueError"
@@ -97,14 +102,66 @@ export class ConflictingMutationError extends Error {
97
102
  }
98
103
  }
99
104
 
100
- /** More attributes were supplied than an entity operation can carry. */
105
+ /**
106
+ * More attributes were supplied than an entity operation can carry — raised by `defineSchema` for a
107
+ * schema with more columns than one create could write.
108
+ */
101
109
  export class TooManyAttributesError extends Error {
102
- constructor(count: number, max: number) {
103
- super(`An entity may carry at most ${max} attributes, got ${count}.`)
110
+ /**
111
+ * @param tagged - Whether `count` includes a named schema's tag, which the message then calls out:
112
+ * the tag is not among the declared columns, so the count would otherwise look off by one.
113
+ */
114
+ constructor(count: number, max: number, tagged = false) {
115
+ super(
116
+ `An entity may carry at most ${max} attributes, got ${count}.` +
117
+ (tagged
118
+ ? ` A named schema's tag counts as one of them, so it may declare at most ${max - 1} ` +
119
+ "columns."
120
+ : ""),
121
+ )
104
122
  this.name = "TooManyAttributesError"
105
123
  }
106
124
  }
107
125
 
126
+ /** The `bytes` values of one create or patch add up to more than `MAX_ENTITY_BYTES`. */
127
+ export class EntityTooLargeError extends Error {
128
+ constructor(total: number, max: number) {
129
+ super(
130
+ `The bytes columns of one create or patch may hold at most ${max} bytes in total, got ` +
131
+ `${total}. Split the data across entities, or store it elsewhere and keep a reference`,
132
+ )
133
+ this.name = "EntityTooLargeError"
134
+ }
135
+ }
136
+
137
+ /** A schema, or one of its columns, was defined in a way the SDK or the protocol cannot honour. */
138
+ export class SchemaDefinitionError extends Error {
139
+ /** The column at fault, when there is one. */
140
+ readonly columnName: string | undefined
141
+
142
+ constructor(reason: string, columnName?: string) {
143
+ super(`Invalid schema${columnName === undefined ? "" : ` column "${columnName}"`}: ${reason}.`)
144
+ this.name = "SchemaDefinitionError"
145
+ this.columnName = columnName
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Data did not fit its schema: a write named a column the schema does not have, left out one it
151
+ * requires, or gave one a value of the wrong type — or an entity read back through a schema lacks a
152
+ * required column or holds one with a different type.
153
+ */
154
+ export class SchemaMismatchError extends Error {
155
+ /** The column at fault. */
156
+ readonly columnName: string
157
+
158
+ constructor(columnName: string, reason: string) {
159
+ super(`Column "${columnName}": ${reason}.`)
160
+ this.name = "SchemaMismatchError"
161
+ this.columnName = columnName
162
+ }
163
+ }
164
+
108
165
  /** The node sent back an attribute this SDK cannot decode. */
109
166
  export class UnknownAttributeTypeError extends Error {
110
167
  /** The type as the node named it — a tag over JSON-RPC, a typeId on the wire. */
@@ -0,0 +1,76 @@
1
+ export type { AttributeSchema, Attributes } from "./attributes"
2
+ export { MAX_ENTITY_BYTES } from "./attributes"
3
+ export type { AnyColumn, Column, ValueArgument } from "./column"
4
+ export { isColumn } from "./column"
5
+ export { DECIMAL_SCALE, DECIMAL_UNIT } from "./decimal"
6
+ export {
7
+ ConflictingMutationError,
8
+ EntityTooLargeError,
9
+ InvalidAttributeNameError,
10
+ InvalidValueError,
11
+ MissingValueError,
12
+ SchemaDefinitionError,
13
+ SchemaMismatchError,
14
+ TooManyAttributesError,
15
+ UnknownAttributeTypeError,
16
+ UntypedValueError,
17
+ } from "./errors"
18
+ export { isValidAttributeName, MAX_NAME_BYTES, validateAttributeName } from "./names"
19
+ export type {
20
+ ColumnInput,
21
+ Columns,
22
+ ColumnsOf,
23
+ CreateEntityValues,
24
+ IndexedColumnName,
25
+ OptionalColumnName,
26
+ PatchEntityValues,
27
+ Schema,
28
+ } from "./schema"
29
+ export {
30
+ defineSchema,
31
+ describeColumns,
32
+ isSchema,
33
+ lookupColumn,
34
+ MAX_ATTRIBUTES,
35
+ SCHEMA_ATTRIBUTE,
36
+ } from "./schema"
37
+ export type {
38
+ AddrValue,
39
+ ArkivValue,
40
+ BoolValue,
41
+ Bytes32Value,
42
+ BytesValue,
43
+ DecValue,
44
+ I32Value,
45
+ IndexableTypeTag,
46
+ IndexableValue,
47
+ KeyValue,
48
+ StrValue,
49
+ TypeTag,
50
+ U64Value,
51
+ U256Value,
52
+ ValueOf,
53
+ } from "./types"
54
+ export { INDEXED_FLAG, isArkivValue } from "./types"
55
+ export type { BareOf, TagOf, ValueInput } from "./values"
56
+ export {
57
+ addr,
58
+ bool,
59
+ bytes,
60
+ bytes32,
61
+ dec,
62
+ decFromUnits,
63
+ decUnits,
64
+ I32_MAX,
65
+ I32_MIN,
66
+ i32,
67
+ key,
68
+ MAX_BYTES_LENGTH,
69
+ MAX_STRING_BYTES,
70
+ str,
71
+ toValue,
72
+ U64_MAX,
73
+ U256_MAX,
74
+ u64,
75
+ u256,
76
+ } from "./values"
@@ -28,6 +28,7 @@ const RESERVED = new Set([
28
28
  "not",
29
29
  "true",
30
30
  "false",
31
+ "between",
31
32
  "startswith",
32
33
  "exists",
33
34
  "typeof",
@@ -49,8 +50,9 @@ const RESERVED = new Set([
49
50
  * reserved words.
50
51
  *
51
52
  * @param name - The attribute name.
52
- * @throws {InvalidAttributeNameError} If the name is empty, too long, uses characters outside the
53
- * grammar, or collides with a reserved word.
53
+ * @throws {InvalidAttributeNameError} If the name is empty, starts with `$` (reserved for system
54
+ * attributes), too long, uses characters outside the grammar, contains `--` (which opens a query
55
+ * comment), or collides with a reserved word.
54
56
  */
55
57
  export function validateAttributeName(name: string): void {
56
58
  if (typeof name !== "string" || name.length === 0) {
@@ -0,0 +1,171 @@
1
+ import { describe, expect, it } from "bun:test"
2
+ import { isColumn } from "./column"
3
+ import {
4
+ InvalidAttributeNameError,
5
+ InvalidValueError,
6
+ SchemaDefinitionError,
7
+ TooManyAttributesError,
8
+ } from "./errors"
9
+ import { defineSchema, isSchema, MAX_ATTRIBUTES, SCHEMA_ATTRIBUTE } from "./schema"
10
+ import { addr, bytes, dec, i32, str, u64 } from "./values"
11
+
12
+ describe("columns", () => {
13
+ it("start unindexed, required and without a default", () => {
14
+ expect(str()).toMatchObject({
15
+ type: "str",
16
+ isIndexed: false,
17
+ isOptional: false,
18
+ defaultValue: undefined,
19
+ })
20
+ })
21
+
22
+ it("refine by returning a new column, leaving the original alone", () => {
23
+ const base = dec()
24
+ const refined = base.indexed().optional()
25
+ expect(refined).toMatchObject({ type: "dec", isIndexed: true, isOptional: true })
26
+ expect(base).toMatchObject({ isIndexed: false, isOptional: false })
27
+ expect(Object.isFrozen(refined)).toBe(true)
28
+ })
29
+
30
+ it("keep every refinement, whatever order they are chained in", () => {
31
+ const a = i32().default(3).indexed().optional()
32
+ const b = i32().optional().indexed().default(3)
33
+ for (const column of [a, b]) {
34
+ expect(column).toMatchObject({ isIndexed: true, isOptional: true, defaultValue: i32(3) })
35
+ }
36
+ })
37
+
38
+ it("take a default as the constructor's argument, or as a value of the column's type", () => {
39
+ expect(dec().default("4.1").defaultValue).toEqual(dec("4.1"))
40
+ expect(dec().default(dec("4.1")).defaultValue).toEqual(dec("4.1"))
41
+ expect(u64().default(5).defaultValue).toEqual(u64(5n))
42
+ expect(bytes().default(new Uint8Array([1])).defaultValue?.value).toBe("0x01")
43
+ })
44
+
45
+ it("validate a default when it is declared, not when it is first written", () => {
46
+ expect(() => dec().default("4.1.1")).toThrow(InvalidValueError)
47
+ expect(() => u64().default(-1)).toThrow(/negative/)
48
+ // A value of another type is not silently converted.
49
+ expect(() => dec().default(i32(4) as never)).toThrow(/i32 value, not a dec/)
50
+ })
51
+
52
+ it("reject a default that is missing or undefined", () => {
53
+ expect(() => (str() as unknown as { default(): unknown }).default()).toThrow(
54
+ SchemaDefinitionError,
55
+ )
56
+ expect(() => str().default(undefined as unknown as string)).toThrow(InvalidValueError)
57
+ })
58
+
59
+ it("never index bytes, even past the type system", () => {
60
+ const column = bytes() as unknown as { indexed(): unknown }
61
+ expect(() => column.indexed()).toThrow(/bytes column cannot be indexed/)
62
+ })
63
+ })
64
+
65
+ describe("defineSchema", () => {
66
+ it("keeps the columns, in the order they were declared", () => {
67
+ const Posts = defineSchema({ title: str(), score: dec().indexed(), body: bytes() })
68
+ expect(isSchema(Posts)).toBe(true)
69
+ expect(Object.keys(Posts.columns)).toEqual(["title", "score", "body"])
70
+ expect(isColumn(Posts.columns.score)).toBe(true)
71
+ expect(Object.isFrozen(Posts.columns)).toBe(true)
72
+ })
73
+
74
+ it("rejects a value where a column belongs, suggesting the default it may have meant", () => {
75
+ const define = () => defineSchema({ score: dec("4.1") } as never)
76
+ expect(define).toThrow(SchemaDefinitionError)
77
+ expect(define).toThrow(/did you mean dec\(\)\.default\("4\.1"\)/i)
78
+ expect(() => defineSchema({ n: u64(5n) } as never)).toThrow(/u64\(\)\.default\(5n\)/)
79
+ })
80
+
81
+ it("rejects anything else that is not a column", () => {
82
+ expect(() => defineSchema({ title: "str" } as never)).toThrow(/not a column/)
83
+ expect(() => defineSchema({ title: str } as never)).toThrow(/not a column/)
84
+ expect(() => defineSchema([] as never)).toThrow(SchemaDefinitionError)
85
+ })
86
+
87
+ it("holds column names to the attribute-name grammar", () => {
88
+ expect(() => defineSchema({ "1st": str() })).toThrow(InvalidAttributeNameError)
89
+ expect(() => defineSchema({ $owner: addr() })).toThrow(/reserved for system attributes/)
90
+ })
91
+
92
+ it("lets a column take a name an entity field has without the $, which a row keeps apart", () => {
93
+ for (const name of ["owner", "creator", "createdAt", "expiresAt", "attributes"]) {
94
+ expect(() => defineSchema({ [name]: str() })).not.toThrow()
95
+ }
96
+ })
97
+
98
+ it("caps the number of columns at what one operation can carry", () => {
99
+ const columns = (count: number) =>
100
+ Object.fromEntries(Array.from({ length: count }, (_, i) => [`c${i}`, i32()]))
101
+ expect(() => defineSchema(columns(MAX_ATTRIBUTES))).not.toThrow()
102
+ expect(() => defineSchema(columns(MAX_ATTRIBUTES + 1))).toThrow(TooManyAttributesError)
103
+ })
104
+
105
+ it("accepts a schema with no columns", () => {
106
+ expect(defineSchema({}).columns).toEqual({})
107
+ })
108
+ })
109
+
110
+ describe("named schemas", () => {
111
+ it("carry their name, and the str tag their entities are marked with", () => {
112
+ const Posts = defineSchema("posts", { title: str() })
113
+ expect(Posts.name).toBe("posts")
114
+ expect(Posts.tag).toEqual(str("posts"))
115
+ expect(Object.keys(Posts.columns)).toEqual(["title"])
116
+ })
117
+
118
+ it("leave an unnamed schema untagged", () => {
119
+ const Posts = defineSchema({ title: str() })
120
+ expect(Posts.name).toBeUndefined()
121
+ expect(Posts.tag).toBeUndefined()
122
+ })
123
+
124
+ it("reject a name that is not a valid str", () => {
125
+ expect(() => defineSchema("", { title: str() })).toThrow(/non-empty string/)
126
+ expect(() => defineSchema("a\nb", { title: str() })).toThrow(SchemaDefinitionError)
127
+ expect(() => defineSchema("x".repeat(129), { title: str() })).toThrow(/not a valid str tag/)
128
+ })
129
+
130
+ it("reserve the tag's attribute name, named or not", () => {
131
+ expect(() => defineSchema({ [SCHEMA_ATTRIBUTE]: str() })).toThrow(
132
+ /the attribute a named schema tags its entities with/,
133
+ )
134
+ expect(() => defineSchema("posts", { [SCHEMA_ATTRIBUTE]: str() })).toThrow(
135
+ SchemaDefinitionError,
136
+ )
137
+ })
138
+
139
+ it("count the tag against the attribute cap", () => {
140
+ const columns = Object.fromEntries(
141
+ Array.from({ length: MAX_ATTRIBUTES }, (_, i) => [`c${i}`, i32()]),
142
+ )
143
+ expect(() => defineSchema(columns)).not.toThrow()
144
+ expect(() => defineSchema("full", columns)).toThrow(/at most 32 attributes, got 33/)
145
+ expect(() => defineSchema("full", columns)).toThrow(/tag counts as one of them/)
146
+ })
147
+ })
148
+
149
+ describe("recognising schemas and columns", () => {
150
+ it("goes by a registry brand, so another copy of the SDK recognises them too", () => {
151
+ // What a schema built by a second copy of this module — the CJS build next to the ESM one —
152
+ // looks like from here: same shape, same registry symbol, a different class.
153
+ const foreign = Object.freeze(
154
+ Object.defineProperty(
155
+ { name: undefined, tag: undefined, columns: {} },
156
+ Symbol.for("@arkiv-network/sdk/schema"),
157
+ { value: true },
158
+ ),
159
+ )
160
+ expect(isSchema(foreign)).toBe(true)
161
+ expect(isSchema(defineSchema({ title: str() }))).toBe(true)
162
+ expect(
163
+ isColumn(Object.defineProperty({}, Symbol.for("@arkiv-network/sdk/column"), { value: true })),
164
+ ).toBe(true)
165
+ })
166
+
167
+ it("does not take a look-alike without the brand", () => {
168
+ expect(isSchema({ columns: {} })).toBe(false)
169
+ expect(isColumn({ type: "str", isIndexed: false, isOptional: false })).toBe(false)
170
+ })
171
+ })