@arkiv-network/sdk 0.6.8 → 0.7.0-dev.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -16
- package/dist/consts-Bz0LZQZY.js +7 -0
- package/dist/consts-Bz0LZQZY.js.map +1 -0
- package/dist/consts-CxT1-RXU.cjs +18 -0
- package/dist/entity-BhSXaB23.cjs +81 -0
- package/dist/{query-CO6f35AE.d.cts → entity-CN41IOrq.d.cts} +125 -59
- package/dist/entity-CN41IOrq.d.cts.map +1 -0
- package/dist/entity-DGfVyB-a.js +71 -0
- package/dist/entity-DGfVyB-a.js.map +1 -0
- package/dist/{query-BhdmAzYg.d.ts → entity-NnEVlePN.d.ts} +125 -59
- package/dist/entity-NnEVlePN.d.ts.map +1 -0
- package/dist/index-ClkpS6s_.d.ts +481 -0
- package/dist/index-ClkpS6s_.d.ts.map +1 -0
- package/dist/index-w4qop0Pe.d.cts +481 -0
- package/dist/index-w4qop0Pe.d.cts.map +1 -0
- package/dist/index.cjs +285 -233
- package/dist/index.d.cts +100 -6
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +101 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +280 -230
- package/dist/index.js.map +1 -1
- package/dist/payload-BpNUPOnY.js +13 -0
- package/dist/payload-BpNUPOnY.js.map +1 -0
- package/dist/{payload-Da9gJMG5.d.ts → payload-C2KHOUoj.d.ts} +1 -1
- package/dist/{payload-Da9gJMG5.d.ts.map → payload-C2KHOUoj.d.ts.map} +1 -1
- package/dist/{payload-vwU3Rc6P.cjs → payload-D8YLKcJj.cjs} +1 -18
- package/dist/query/index.cjs +11 -28
- package/dist/query/index.d.cts +3 -3
- package/dist/query/index.d.ts +3 -3
- package/dist/query/index.js +8 -28
- package/dist/query/index.js.map +1 -1
- package/dist/{queryBuilder-DonAg1aq.js → queryBuilder-BE_XhLLa.js} +303 -152
- package/dist/queryBuilder-BE_XhLLa.js.map +1 -0
- package/dist/{queryBuilder-DM-YO1Y7.cjs → queryBuilder-J37buGCi.cjs} +372 -149
- package/dist/types/index.cjs +4 -1
- package/dist/types/index.d.cts +2 -2
- package/dist/types/index.d.ts +2 -2
- package/dist/types/index.js +4 -1
- package/dist/utils/index.cjs +4 -10
- package/dist/utils/index.d.cts +0 -1
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.ts +1 -2
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +2 -3
- package/dist/utils/index.js.map +1 -1
- package/package.json +11 -15
- package/src/actions/public/buildQuery.ts +4 -0
- package/src/actions/public/query.ts +10 -0
- package/src/actions/public/subscribeEntityEvents.ts +25 -51
- package/src/actions/wallet/changeOwnership.ts +6 -6
- package/src/actions/wallet/createEntity.ts +19 -6
- package/src/actions/wallet/deleteEntity.ts +2 -3
- package/src/actions/wallet/extendEntity.ts +9 -5
- package/src/actions/wallet/mutateEntities.ts +11 -54
- package/src/actions/wallet/updateEntity.ts +15 -6
- package/src/clients/createPublicClient.test.ts +2 -0
- package/src/clients/decorators/arkivPublic.ts +67 -2
- package/src/clients/decorators/arkivWallet.ts +23 -1
- package/src/consts.ts +2 -1
- package/src/errors.ts +46 -0
- package/src/index.ts +0 -3
- package/src/query/engine.test.ts +45 -0
- package/src/query/engine.ts +28 -14
- package/src/query/index.ts +1 -0
- package/src/query/predicate.test.ts +43 -0
- package/src/query/predicate.ts +14 -10
- package/src/query/queryBuilder.test.ts +22 -0
- package/src/query/queryBuilder.ts +221 -106
- package/src/query/queryResult.ts +13 -7
- package/src/query/selectQueryBuilder.test.ts +213 -0
- package/src/query/selection.test.ts +120 -0
- package/src/query/selection.ts +116 -0
- package/src/types/attributes.ts +25 -1
- package/src/types/entity.ts +12 -2
- package/src/types/events.ts +14 -16
- package/src/types/index.ts +1 -0
- package/src/types/rpcSchema.ts +7 -2
- package/src/utils/arkivTransactions.test.ts +260 -0
- package/src/utils/arkivTransactions.ts +255 -92
- package/src/utils/entities.ts +17 -7
- package/src/utils/index.ts +0 -1
- package/src/utils/payload.ts +4 -2
- package/src/utils/validation.test.ts +106 -0
- package/src/utils/validation.ts +80 -0
- package/dist/accounts/index.cjs +0 -9
- package/dist/accounts/index.d.cts +0 -1
- package/dist/accounts/index.d.ts +0 -1
- package/dist/accounts/index.js +0 -1
- package/dist/payload-DKBYYjeA.js +0 -18
- package/dist/payload-DKBYYjeA.js.map +0 -1
- package/dist/query-BhdmAzYg.d.ts.map +0 -1
- package/dist/query-CO6f35AE.d.cts.map +0 -1
- package/dist/queryBuilder-BvoQXa_u.d.ts +0 -328
- package/dist/queryBuilder-BvoQXa_u.d.ts.map +0 -1
- package/dist/queryBuilder-DonAg1aq.js.map +0 -1
- package/dist/queryBuilder-zTsKXctT.d.cts +0 -328
- package/dist/queryBuilder-zTsKXctT.d.cts.map +0 -1
- package/src/accounts/index.ts +0 -1
- /package/dist/{types-H-TSmRpm.js → types-Ci4BRClw.js} +0 -0
- /package/dist/{types-o3zWarRp.cjs → types-a8gm_IaQ.cjs} +0 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, jest, test } from "bun:test"
|
|
2
|
+
import type { ArkivClient } from "../clients/baseClient"
|
|
3
|
+
import type { Entity } from "../types/entity"
|
|
4
|
+
import { RpcAttributeValueType } from "../types/rpcSchema"
|
|
5
|
+
import * as entitiesUtils from "../utils/entities"
|
|
6
|
+
import * as engine from "./engine"
|
|
7
|
+
import { eq, gte } from "./predicate"
|
|
8
|
+
import { SelectQueryBuilder } from "./queryBuilder"
|
|
9
|
+
import { selectionToIncludeData } from "./selection"
|
|
10
|
+
|
|
11
|
+
describe("SelectQueryBuilder", () => {
|
|
12
|
+
let mockClient: ArkivClient
|
|
13
|
+
let mockProcessQuery: any
|
|
14
|
+
|
|
15
|
+
beforeEach(() => {
|
|
16
|
+
mockClient = {
|
|
17
|
+
request: jest.fn(),
|
|
18
|
+
} as unknown as ArkivClient
|
|
19
|
+
|
|
20
|
+
mockProcessQuery = jest.spyOn(engine, "processQuery")
|
|
21
|
+
jest.spyOn(entitiesUtils, "entityFromRpcResult")
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
jest.restoreAllMocks()
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
test("select() with no argument forwards include-data for everything", async () => {
|
|
29
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
30
|
+
|
|
31
|
+
await new SelectQueryBuilder(mockClient).where(eq("name", "test")).fetch()
|
|
32
|
+
|
|
33
|
+
expect(mockProcessQuery).toHaveBeenCalledWith(
|
|
34
|
+
mockClient,
|
|
35
|
+
expect.objectContaining({
|
|
36
|
+
predicates: [eq("name", "test")],
|
|
37
|
+
includeData: selectionToIncludeData(),
|
|
38
|
+
}),
|
|
39
|
+
)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
test('select("*") forwards include-data for everything', async () => {
|
|
43
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
44
|
+
|
|
45
|
+
await new SelectQueryBuilder(mockClient, "*").fetch()
|
|
46
|
+
|
|
47
|
+
expect(mockProcessQuery).toHaveBeenCalledWith(
|
|
48
|
+
mockClient,
|
|
49
|
+
expect.objectContaining({
|
|
50
|
+
includeData: selectionToIncludeData("*"),
|
|
51
|
+
}),
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("select({ owner: true, attributes: true }) forwards the resolved include-data", async () => {
|
|
56
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
57
|
+
|
|
58
|
+
await new SelectQueryBuilder(mockClient, { owner: true, attributes: true }).fetch()
|
|
59
|
+
|
|
60
|
+
expect(mockProcessQuery).toHaveBeenCalledWith(
|
|
61
|
+
mockClient,
|
|
62
|
+
expect.objectContaining({
|
|
63
|
+
includeData: selectionToIncludeData({ owner: true, attributes: true }),
|
|
64
|
+
}),
|
|
65
|
+
)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("select({ owner: true }) forwards narrowed include-data", async () => {
|
|
69
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
70
|
+
|
|
71
|
+
await new SelectQueryBuilder(mockClient, { owner: true }).fetch()
|
|
72
|
+
|
|
73
|
+
const call = mockProcessQuery.mock.calls[0][1]
|
|
74
|
+
expect(call.includeData).toEqual(selectionToIncludeData({ owner: true }))
|
|
75
|
+
expect(call.includeData.owner).toBe(true)
|
|
76
|
+
expect(call.includeData.creator).toBe(false)
|
|
77
|
+
// key is opt-in: not selected here, so it is excluded
|
|
78
|
+
expect(call.includeData.key).toBe(false)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
test("does not forward the with* boolean flags", async () => {
|
|
82
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
83
|
+
|
|
84
|
+
await new SelectQueryBuilder(mockClient, { attributes: true }).fetch()
|
|
85
|
+
|
|
86
|
+
const call = mockProcessQuery.mock.calls[0][1]
|
|
87
|
+
expect(call.withAttributes).toBeUndefined()
|
|
88
|
+
expect(call.withMetadata).toBeUndefined()
|
|
89
|
+
expect(call.withPayload).toBeUndefined()
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test("shares the filtering/ordering/pagination chain methods", async () => {
|
|
93
|
+
mockProcessQuery.mockResolvedValue({ data: [] })
|
|
94
|
+
|
|
95
|
+
const owner = "0x1234567890123456789012345678901234567890" as const
|
|
96
|
+
await new SelectQueryBuilder(mockClient, { attributes: true })
|
|
97
|
+
.where(eq("name", "test"))
|
|
98
|
+
.where(gte("age", 18))
|
|
99
|
+
.ownedBy(owner)
|
|
100
|
+
.createdBy(owner)
|
|
101
|
+
.orderBy("name", "string", "desc")
|
|
102
|
+
.limit(10)
|
|
103
|
+
.cursor("0xABC123")
|
|
104
|
+
.validAtBlock(123n)
|
|
105
|
+
.fetch()
|
|
106
|
+
|
|
107
|
+
expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
|
|
108
|
+
predicates: [eq("name", "test"), gte("age", 18)],
|
|
109
|
+
limit: 10,
|
|
110
|
+
cursor: "0xABC123",
|
|
111
|
+
ownedBy: owner,
|
|
112
|
+
createdBy: owner,
|
|
113
|
+
orderBy: [{ name: "name", type: "string", desc: true }],
|
|
114
|
+
validAtBlock: 123n,
|
|
115
|
+
includeData: selectionToIncludeData({ attributes: true }),
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
test("count() ignores the selection and requests no data", async () => {
|
|
120
|
+
mockProcessQuery.mockResolvedValue({
|
|
121
|
+
data: [
|
|
122
|
+
{ key: "0xabc", value: "data1" },
|
|
123
|
+
{ key: "0xdef", value: "data2" },
|
|
124
|
+
],
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
const count = await new SelectQueryBuilder(mockClient, "*")
|
|
128
|
+
.where(eq("status", "active"))
|
|
129
|
+
.count()
|
|
130
|
+
|
|
131
|
+
expect(count).toBe(2)
|
|
132
|
+
expect(mockProcessQuery).toHaveBeenCalledWith(mockClient, {
|
|
133
|
+
predicates: [eq("status", "active")],
|
|
134
|
+
limit: undefined,
|
|
135
|
+
cursor: undefined,
|
|
136
|
+
ownedBy: undefined,
|
|
137
|
+
createdBy: undefined,
|
|
138
|
+
orderBy: undefined,
|
|
139
|
+
validAtBlock: undefined,
|
|
140
|
+
withAttributes: false,
|
|
141
|
+
withMetadata: false,
|
|
142
|
+
withPayload: false,
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
|
|
146
|
+
describe("result entities", () => {
|
|
147
|
+
test("fetch returns flat Entity instances with the selected data and methods", async () => {
|
|
148
|
+
mockProcessQuery.mockResolvedValue({
|
|
149
|
+
data: [
|
|
150
|
+
{
|
|
151
|
+
key: "0xabc",
|
|
152
|
+
owner: "0xowner",
|
|
153
|
+
value: "0x68656c6c6f", // "hello"
|
|
154
|
+
attributes: [
|
|
155
|
+
{ key: "category", value: "docs", valueType: RpcAttributeValueType.String },
|
|
156
|
+
{ key: "score", value: "0x2a", valueType: RpcAttributeValueType.Uint },
|
|
157
|
+
],
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
cursor: "",
|
|
161
|
+
blockNumber: "0x1",
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
const result = await new SelectQueryBuilder(mockClient, {
|
|
165
|
+
key: true,
|
|
166
|
+
owner: true,
|
|
167
|
+
attributes: true,
|
|
168
|
+
payload: true,
|
|
169
|
+
}).fetch()
|
|
170
|
+
|
|
171
|
+
expect(result.entities).toHaveLength(1)
|
|
172
|
+
const entity = result.entities[0] as unknown as Entity
|
|
173
|
+
// flat, backwards-compatible field access
|
|
174
|
+
expect(entity.key).toBe("0xabc")
|
|
175
|
+
expect(entity.owner).toBe("0xowner")
|
|
176
|
+
expect(entity.attributes).toEqual([
|
|
177
|
+
{ key: "category", value: "docs" },
|
|
178
|
+
{ key: "score", value: 42 },
|
|
179
|
+
])
|
|
180
|
+
// entity methods are preserved on the result
|
|
181
|
+
expect(typeof entity.toText).toBe("function")
|
|
182
|
+
expect(entity.toText()).toBe("hello")
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test("toJson decodes a JSON payload", async () => {
|
|
186
|
+
mockProcessQuery.mockResolvedValue({
|
|
187
|
+
data: [{ key: "0xabc", value: "0x7b2261223a317d" }], // {"a":1}
|
|
188
|
+
cursor: "",
|
|
189
|
+
blockNumber: "0x1",
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
const result = await new SelectQueryBuilder(mockClient, { payload: true }).fetch()
|
|
193
|
+
const entity = result.entities[0] as unknown as Entity
|
|
194
|
+
expect(entity.toJson()).toEqual({ a: 1 })
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
test("fields the RPC omits (because they were not selected) are undefined at runtime", async () => {
|
|
198
|
+
// The RPC honours includeData and returns only the key.
|
|
199
|
+
mockProcessQuery.mockResolvedValue({
|
|
200
|
+
data: [{ key: "0xabc" }],
|
|
201
|
+
cursor: "",
|
|
202
|
+
blockNumber: "0x1",
|
|
203
|
+
})
|
|
204
|
+
|
|
205
|
+
const result = await new SelectQueryBuilder(mockClient, { key: true }).fetch()
|
|
206
|
+
const entity = result.entities[0] as unknown as Entity
|
|
207
|
+
expect(entity.key).toBe("0xabc")
|
|
208
|
+
expect(entity.owner).toBeUndefined()
|
|
209
|
+
expect(entity.payload).toBeUndefined()
|
|
210
|
+
expect(entity.attributes).toEqual([])
|
|
211
|
+
})
|
|
212
|
+
})
|
|
213
|
+
})
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test"
|
|
2
|
+
import { selectionToIncludeData } from "./selection"
|
|
3
|
+
|
|
4
|
+
const ALL = {
|
|
5
|
+
key: true,
|
|
6
|
+
attributes: true,
|
|
7
|
+
payload: true,
|
|
8
|
+
contentType: true,
|
|
9
|
+
expiration: true,
|
|
10
|
+
owner: true,
|
|
11
|
+
creator: true,
|
|
12
|
+
createdAtBlock: true,
|
|
13
|
+
lastModifiedAtBlock: true,
|
|
14
|
+
transactionIndexInBlock: true,
|
|
15
|
+
operationIndexInTransaction: true,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const EMPTY = {
|
|
19
|
+
key: false,
|
|
20
|
+
attributes: false,
|
|
21
|
+
payload: false,
|
|
22
|
+
contentType: false,
|
|
23
|
+
expiration: false,
|
|
24
|
+
owner: false,
|
|
25
|
+
creator: false,
|
|
26
|
+
createdAtBlock: false,
|
|
27
|
+
lastModifiedAtBlock: false,
|
|
28
|
+
transactionIndexInBlock: false,
|
|
29
|
+
operationIndexInTransaction: false,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("selectionToIncludeData", () => {
|
|
33
|
+
test("select() with no argument selects everything", () => {
|
|
34
|
+
expect(selectionToIncludeData()).toEqual(ALL)
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
test('select("*") selects everything', () => {
|
|
38
|
+
expect(selectionToIncludeData("*")).toEqual(ALL)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test("empty object throws", () => {
|
|
42
|
+
// @ts-expect-error – select requires at least one field
|
|
43
|
+
expect(() => selectionToIncludeData({})).toThrow()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("an all-false selection throws (no field is actually selected)", () => {
|
|
47
|
+
expect(() => selectionToIncludeData({ key: false })).toThrow()
|
|
48
|
+
expect(() => selectionToIncludeData({ owner: false, payload: false })).toThrow()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
test("key: true selects only the key", () => {
|
|
52
|
+
expect(selectionToIncludeData({ key: true })).toEqual({ ...EMPTY, key: true })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test("key is opt-in like every other field", () => {
|
|
56
|
+
expect(selectionToIncludeData({ key: true }).key).toBe(true)
|
|
57
|
+
expect(selectionToIncludeData({ attributes: true }).key).toBe(false)
|
|
58
|
+
expect(selectionToIncludeData({ owner: true }).key).toBe(false)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test("attributes only (no key)", () => {
|
|
62
|
+
expect(selectionToIncludeData({ attributes: true })).toEqual({
|
|
63
|
+
...EMPTY,
|
|
64
|
+
attributes: true,
|
|
65
|
+
})
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
test("payload only (no key)", () => {
|
|
69
|
+
expect(selectionToIncludeData({ payload: true })).toEqual({
|
|
70
|
+
...EMPTY,
|
|
71
|
+
payload: true,
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
test("a single metadata field, selected flat (no key)", () => {
|
|
76
|
+
expect(selectionToIncludeData({ owner: true })).toEqual({
|
|
77
|
+
...EMPTY,
|
|
78
|
+
owner: true,
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test("expiresAtBlock maps to the RPC `expiration` field", () => {
|
|
83
|
+
expect(selectionToIncludeData({ expiresAtBlock: true })).toEqual({
|
|
84
|
+
...EMPTY,
|
|
85
|
+
expiration: true,
|
|
86
|
+
})
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test("several fields selected together", () => {
|
|
90
|
+
expect(
|
|
91
|
+
selectionToIncludeData({
|
|
92
|
+
owner: true,
|
|
93
|
+
creator: true,
|
|
94
|
+
createdAtBlock: true,
|
|
95
|
+
attributes: true,
|
|
96
|
+
}),
|
|
97
|
+
).toEqual({
|
|
98
|
+
...EMPTY,
|
|
99
|
+
owner: true,
|
|
100
|
+
creator: true,
|
|
101
|
+
createdAtBlock: true,
|
|
102
|
+
attributes: true,
|
|
103
|
+
})
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("key can be combined with other fields", () => {
|
|
107
|
+
expect(selectionToIncludeData({ key: true, attributes: true })).toEqual({
|
|
108
|
+
...EMPTY,
|
|
109
|
+
key: true,
|
|
110
|
+
attributes: true,
|
|
111
|
+
})
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
test("a field set to false is not selected", () => {
|
|
115
|
+
expect(selectionToIncludeData({ owner: true, creator: false })).toEqual({
|
|
116
|
+
...EMPTY,
|
|
117
|
+
owner: true,
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
})
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import type { Entity } from "../types/entity"
|
|
2
|
+
import type { RpcIncludeData } from "../types/rpcSchema"
|
|
3
|
+
|
|
4
|
+
/** The entity fields that can be selected. These match the fields of {@link Entity} one-to-one. */
|
|
5
|
+
type DataKey =
|
|
6
|
+
| "key"
|
|
7
|
+
| "contentType"
|
|
8
|
+
| "owner"
|
|
9
|
+
| "creator"
|
|
10
|
+
| "expiresAtBlock"
|
|
11
|
+
| "createdAtBlock"
|
|
12
|
+
| "lastModifiedAtBlock"
|
|
13
|
+
| "transactionIndexInBlock"
|
|
14
|
+
| "operationIndexInTransaction"
|
|
15
|
+
| "attributes"
|
|
16
|
+
| "payload"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The parts of an entity that can be selected. Every field is opt-in (including the `key`) and maps
|
|
20
|
+
* directly to a field of the returned {@link Entity} — the selection is flat.
|
|
21
|
+
*/
|
|
22
|
+
export type SelectionFields = { [K in DataKey]?: boolean }
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Requires at least one property of `T` to be present.
|
|
26
|
+
* `{}` is not assignable, which is how `select({})` is rejected at compile time.
|
|
27
|
+
*/
|
|
28
|
+
type AtLeastOne<T, K extends keyof T = keyof T> = {
|
|
29
|
+
[P in K]: Required<Pick<T, P>> & Partial<Omit<T, P>>
|
|
30
|
+
}[K]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* A non-empty entity selection — the object form accepted by `select()`. At least one field must be
|
|
34
|
+
* selected.
|
|
35
|
+
*/
|
|
36
|
+
export type EntitySelection = AtLeastOne<SelectionFields>
|
|
37
|
+
|
|
38
|
+
/** Argument accepted by `select()` — a non-empty {@link EntitySelection}, or `"*"` for everything. */
|
|
39
|
+
export type SelectArg = EntitySelection | "*"
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* The result of a selection `S`: an {@link Entity} narrowed to exactly the selected fields (each
|
|
43
|
+
* non-`undefined`). The shape is flat and backwards compatible — e.g. `select({ owner: true })`
|
|
44
|
+
* yields `{ owner: Hex }` accessed as `entity.owner`. Accessing an unselected field is a compile
|
|
45
|
+
* error.
|
|
46
|
+
*
|
|
47
|
+
* The `toText()` / `toJson()` helpers operate on the payload, so they are only present when
|
|
48
|
+
* `payload` was selected — calling them otherwise would always throw.
|
|
49
|
+
*/
|
|
50
|
+
export type ProjectedEntity<S> = {
|
|
51
|
+
readonly [K in keyof S as S[K] extends true ? K : never]: K extends keyof Entity
|
|
52
|
+
? NonNullable<Entity[K]>
|
|
53
|
+
: never
|
|
54
|
+
} & (S extends { payload: true } ? Pick<Entity, "toText" | "toJson"> : unknown)
|
|
55
|
+
|
|
56
|
+
/** A selection of every field, used to type the result of `select()` / `select("*")`. */
|
|
57
|
+
type AllSelected = { [K in DataKey]: true }
|
|
58
|
+
|
|
59
|
+
/** The projected entity type when everything is selected — every field, plus the methods. */
|
|
60
|
+
export type FullEntity = ProjectedEntity<AllSelected>
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Converts a {@link SelectArg} into the {@link RpcIncludeData} understood by the query engine.
|
|
64
|
+
* Every field is opt-in — anything not explicitly selected defaults to `false`, including the key.
|
|
65
|
+
* `undefined` or `"*"` selects everything.
|
|
66
|
+
*
|
|
67
|
+
* @param selection - The selection to convert
|
|
68
|
+
* @returns The fully-resolved include-data object for the RPC query
|
|
69
|
+
* @throws If an empty selection object is provided
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* selectionToIncludeData() // everything
|
|
73
|
+
* selectionToIncludeData("*") // everything
|
|
74
|
+
* selectionToIncludeData({ key: true }) // only the key
|
|
75
|
+
* selectionToIncludeData({ owner: true }) // only the owner
|
|
76
|
+
*/
|
|
77
|
+
export function selectionToIncludeData(selection?: SelectArg): RpcIncludeData {
|
|
78
|
+
if (selection === undefined || selection === "*") {
|
|
79
|
+
return {
|
|
80
|
+
key: true,
|
|
81
|
+
attributes: true,
|
|
82
|
+
payload: true,
|
|
83
|
+
contentType: true,
|
|
84
|
+
expiration: true,
|
|
85
|
+
owner: true,
|
|
86
|
+
creator: true,
|
|
87
|
+
createdAtBlock: true,
|
|
88
|
+
lastModifiedAtBlock: true,
|
|
89
|
+
transactionIndexInBlock: true,
|
|
90
|
+
operationIndexInTransaction: true,
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// At least one field must be opted in. This rejects both `{}` and selections whose fields are
|
|
95
|
+
// all explicitly `false` (e.g. `select({ key: false })`), which the type system permits.
|
|
96
|
+
if (!Object.values(selection).some((selected) => selected === true)) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
'select() requires at least one field to be selected. Pass select() or select("*") to select everything.',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
key: selection.key ?? false,
|
|
104
|
+
attributes: selection.attributes ?? false,
|
|
105
|
+
payload: selection.payload ?? false,
|
|
106
|
+
contentType: selection.contentType ?? false,
|
|
107
|
+
// The selection uses the entity field name `expiresAtBlock`; the RPC calls it `expiration`.
|
|
108
|
+
expiration: selection.expiresAtBlock ?? false,
|
|
109
|
+
owner: selection.owner ?? false,
|
|
110
|
+
creator: selection.creator ?? false,
|
|
111
|
+
createdAtBlock: selection.createdAtBlock ?? false,
|
|
112
|
+
lastModifiedAtBlock: selection.lastModifiedAtBlock ?? false,
|
|
113
|
+
transactionIndexInBlock: selection.transactionIndexInBlock ?? false,
|
|
114
|
+
operationIndexInTransaction: selection.operationIndexInTransaction ?? false,
|
|
115
|
+
}
|
|
116
|
+
}
|
package/src/types/attributes.ts
CHANGED
|
@@ -1,4 +1,28 @@
|
|
|
1
|
+
import type { Hex } from "viem"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* An entity attribute (key/value pair).
|
|
5
|
+
*
|
|
6
|
+
* Arkiv supports string, Hex and number values.
|
|
7
|
+
*
|
|
8
|
+
* Hex values are stored as the on-chain `EntityKey` type **only when they are
|
|
9
|
+
* exactly 32 bytes** (66 characters, e.g. an entity key). Hex-looking strings
|
|
10
|
+
* of any other length are stored as plain strings: they round-trip unchanged
|
|
11
|
+
* and compare lexicographically, not as hex.
|
|
12
|
+
*
|
|
13
|
+
* Numeric values **must be non-negative integers** (they are stored on-chain
|
|
14
|
+
* as unsigned 256-bit) — passing a non-integer (e.g. `1.5`) or negative number
|
|
15
|
+
* throws an {@link InvalidAttributeError} at mutation time. To store a
|
|
16
|
+
* non-integer value:
|
|
17
|
+
* - Scale it to an integer (e.g. `1.5` -> `1500`, dividing by the same factor on
|
|
18
|
+
* read) to keep numeric ordering and range queries working.
|
|
19
|
+
* - Or pass it as a string (e.g. `"1.5"`), which sorts lexicographically and so
|
|
20
|
+
* does not support numeric comparisons.
|
|
21
|
+
*
|
|
22
|
+
* To store a negative value, offset it into the non-negative range (subtracting
|
|
23
|
+
* the offset on read) or store it as a string.
|
|
24
|
+
*/
|
|
1
25
|
export type Attribute = {
|
|
2
26
|
key: string
|
|
3
|
-
value: string |
|
|
27
|
+
value: number | string | Hex
|
|
4
28
|
}
|
package/src/types/entity.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { bytesToString, type Hex } from "viem"
|
|
2
|
+
|
|
3
|
+
export enum EntityOperationType {
|
|
4
|
+
Create = 1,
|
|
5
|
+
Update = 2,
|
|
6
|
+
Extend = 3,
|
|
7
|
+
Transfer = 4,
|
|
8
|
+
Delete = 5,
|
|
9
|
+
Expire = 6,
|
|
10
|
+
}
|
|
11
|
+
|
|
2
12
|
import type { MimeType } from "../types"
|
|
3
13
|
import type { Attribute } from "./attributes"
|
|
4
14
|
|
|
5
15
|
export class Entity {
|
|
6
16
|
key: Hex
|
|
7
|
-
contentType: MimeType | undefined
|
|
17
|
+
contentType: MimeType | `${string}/${string}` | undefined
|
|
8
18
|
owner: Hex | undefined
|
|
9
19
|
creator: Hex | undefined
|
|
10
20
|
expiresAtBlock: bigint | undefined
|
|
@@ -17,7 +27,7 @@ export class Entity {
|
|
|
17
27
|
|
|
18
28
|
constructor(
|
|
19
29
|
key: Hex,
|
|
20
|
-
contentType: MimeType | undefined = undefined,
|
|
30
|
+
contentType: MimeType | `${string}/${string}` | undefined = undefined,
|
|
21
31
|
owner: Hex | undefined = undefined,
|
|
22
32
|
creator: Hex | undefined = undefined,
|
|
23
33
|
expiresAtBlock: bigint | undefined = undefined,
|
package/src/types/events.ts
CHANGED
|
@@ -1,40 +1,38 @@
|
|
|
1
|
-
import type { Hex } from "viem"
|
|
1
|
+
import type { Address, Hex } from "viem"
|
|
2
2
|
|
|
3
3
|
export type OnEntityCreatedEvent = {
|
|
4
4
|
entityKey: Hex
|
|
5
|
-
owner:
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
owner: Address
|
|
6
|
+
expiresAt: number
|
|
7
|
+
entityHash: Hex
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export type OnEntityUpdatedEvent = {
|
|
11
11
|
entityKey: Hex
|
|
12
|
-
owner:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
cost: bigint
|
|
12
|
+
owner: Address
|
|
13
|
+
expiresAt: number
|
|
14
|
+
entityHash: Hex
|
|
16
15
|
}
|
|
17
16
|
|
|
18
17
|
export type OnEntityDeletedEvent = {
|
|
19
18
|
entityKey: Hex
|
|
20
|
-
owner:
|
|
19
|
+
owner: Address
|
|
21
20
|
}
|
|
22
21
|
|
|
23
22
|
export type OnEntityExpiredEvent = {
|
|
24
23
|
entityKey: Hex
|
|
25
|
-
owner:
|
|
24
|
+
owner: Address
|
|
26
25
|
}
|
|
27
26
|
|
|
28
27
|
export type OnEntityExpiresInExtendedEvent = {
|
|
29
28
|
entityKey: Hex
|
|
30
|
-
owner:
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
cost: bigint
|
|
29
|
+
owner: Address
|
|
30
|
+
expiresAt: number
|
|
31
|
+
entityHash: Hex
|
|
34
32
|
}
|
|
35
33
|
|
|
36
34
|
export type OnEntityOwnerChangedEvent = {
|
|
37
35
|
entityKey: Hex
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
owner: Address
|
|
37
|
+
entityHash: Hex
|
|
40
38
|
}
|
package/src/types/index.ts
CHANGED
|
@@ -19,6 +19,7 @@ export type {
|
|
|
19
19
|
export type { UpdateEntityParameters, UpdateEntityReturnType } from "../actions/wallet/updateEntity"
|
|
20
20
|
export type { Attribute } from "./attributes"
|
|
21
21
|
export type { Entity } from "./entity"
|
|
22
|
+
export { EntityOperationType } from "./entity"
|
|
22
23
|
export type {
|
|
23
24
|
OnEntityCreatedEvent,
|
|
24
25
|
OnEntityDeletedEvent,
|
package/src/types/rpcSchema.ts
CHANGED
|
@@ -12,8 +12,13 @@ export type RpcEntity = {
|
|
|
12
12
|
operationIndexInTransaction: Hex
|
|
13
13
|
owner: Hex
|
|
14
14
|
creator: Hex
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
attributes?: { key: string; value: string; valueType: RpcAttributeValueType }[]
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export enum RpcAttributeValueType {
|
|
19
|
+
Uint = 1,
|
|
20
|
+
String = 2,
|
|
21
|
+
EntityKey = 3,
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
export type RpcOrderByAttribute = {
|