@arkiv-network/sdk 0.1.1-dev.0 → 0.1.2-dev.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -10
- package/dist/chains/index.cjs +1 -0
- package/dist/chains/index.d.ts +25 -25
- package/dist/chains/index.d.ts.map +1 -1
- package/dist/index.cjs +40 -10
- package/dist/index.js +13 -8
- package/dist/index.js.map +1 -1
- package/dist/query/index.cjs +1 -1
- package/dist/{queryBuilder-n9ocnT4P.cjs → queryBuilder-okYHE7jd.cjs} +1 -0
- package/dist/utils/index.cjs +1 -0
- package/dist/utils/index.d.cts.map +1 -1
- package/dist/utils/index.d.ts.map +1 -1
- package/package.json +22 -8
- package/src/accounts/index.ts +1 -0
- package/src/actions/public/buildQuery.ts +6 -0
- package/src/actions/public/getBlockTiming.ts +20 -0
- package/src/actions/public/getEntity.ts +18 -0
- package/src/actions/public/getEntityCount.ts +10 -0
- package/src/actions/public/query.ts +13 -0
- package/src/actions/public/subscribeEntityEvents.ts +105 -0
- package/src/actions/wallet/changeOwnership.ts +40 -0
- package/src/actions/wallet/createEntity.ts +45 -0
- package/src/actions/wallet/deleteEntity.ts +39 -0
- package/src/actions/wallet/extendEntity.ts +41 -0
- package/src/actions/wallet/mutateEntities.ts +100 -0
- package/src/actions/wallet/updateEntity.ts +47 -0
- package/src/chains/index.ts +3 -0
- package/src/chains/kaolin.ts +26 -0
- package/src/chains/localhost.ts +14 -0
- package/src/chains/marketplace.ts +26 -0
- package/src/clients/baseClient.ts +9 -0
- package/src/clients/createPublicClient.test.ts +22 -0
- package/src/clients/createPublicClient.ts +65 -0
- package/src/clients/createWalletClient.test.ts +23 -0
- package/src/clients/createWalletClient.ts +70 -0
- package/src/clients/decorators/arkivPublic.ts +203 -0
- package/src/clients/decorators/arkivWallet.ts +250 -0
- package/src/consts.ts +2 -0
- package/src/index.ts +21 -0
- package/src/query/engine.test.ts +288 -0
- package/src/query/engine.ts +112 -0
- package/src/query/errors.ts +23 -0
- package/src/query/index.ts +4 -0
- package/src/query/predicate.test.ts +418 -0
- package/src/query/predicate.ts +140 -0
- package/src/query/queryBuilder.test.ts +537 -0
- package/src/query/queryBuilder.ts +209 -0
- package/src/query/queryResult.test.ts +270 -0
- package/src/query/queryResult.ts +61 -0
- package/src/types/attributes.ts +4 -0
- package/src/types/entity.ts +36 -0
- package/src/types/events.ts +34 -0
- package/src/types/index.ts +4 -0
- package/src/types/mimeTypes.ts +27 -0
- package/src/types/rpcSchema.ts +61 -0
- package/src/types/txParams.ts +15 -0
- package/src/utils/arkivTransactions.ts +100 -0
- package/src/utils/compression.test.ts +32 -0
- package/src/utils/compression.ts +48 -0
- package/src/utils/entities.ts +23 -0
- package/src/utils/expirationTime.ts +13 -0
- package/src/utils/index.ts +3 -0
- package/src/utils/payload.ts +9 -0
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type { Hex } from "viem"
|
|
2
|
+
import type { ArkivClient } from "../clients/baseClient"
|
|
3
|
+
import { entityFromRpcResult } from "../utils/entities"
|
|
4
|
+
import { processQuery } from "./engine"
|
|
5
|
+
import type { Predicate } from "./predicate"
|
|
6
|
+
import { QueryResult } from "./queryResult"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* QueryBuilder is a helper class to build queries to the Arkiv DBChains.
|
|
10
|
+
* It can be used to fetch entities from the Arkiv DBChains. It follows the Builder pattern allowing chaining of methods.
|
|
11
|
+
* @param client - The Arkiv client
|
|
12
|
+
* @returns The QueryBuilder instance {@link QueryBuilder}
|
|
13
|
+
*/
|
|
14
|
+
export class QueryBuilder {
|
|
15
|
+
private _client: ArkivClient
|
|
16
|
+
private _ownedBy: Hex | undefined
|
|
17
|
+
private _validAtBlock: bigint | undefined
|
|
18
|
+
private _withAttributes: boolean | undefined
|
|
19
|
+
private _withMetadata: boolean | undefined
|
|
20
|
+
private _withPayload: boolean | undefined
|
|
21
|
+
private _limit: number | undefined
|
|
22
|
+
private _cursor: string | undefined
|
|
23
|
+
private _predicates: Predicate[]
|
|
24
|
+
|
|
25
|
+
constructor(client: ArkivClient) {
|
|
26
|
+
this._client = client
|
|
27
|
+
this._predicates = []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Sets the ownedBy filter
|
|
32
|
+
* @param ownedBy - The address of the owner
|
|
33
|
+
* @returns The QueryBuilder instance
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* const builder = new QueryBuilder(client)
|
|
37
|
+
* builder.ownedBy("0x1234567890123456789012345678901234567890")
|
|
38
|
+
*/
|
|
39
|
+
ownedBy(ownedBy: Hex) {
|
|
40
|
+
this._ownedBy = ownedBy
|
|
41
|
+
return this
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Sets the withAttributes flag which will return the attributes for the entities if true
|
|
46
|
+
* @param withAttributes - The boolean value to set
|
|
47
|
+
* @returns The QueryBuilder instance
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* const builder = new QueryBuilder(client)
|
|
51
|
+
* builder.withAttributes(true)
|
|
52
|
+
*/
|
|
53
|
+
withAttributes(withAttributes: boolean = true) {
|
|
54
|
+
this._withAttributes = withAttributes
|
|
55
|
+
return this
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Sets the withMetadata flag which will return the metadata (like owner, expiredAt, etc.) for the entities if true
|
|
60
|
+
* @param withMetadata - The boolean value to set
|
|
61
|
+
* @returns The QueryBuilder instance
|
|
62
|
+
*
|
|
63
|
+
* @example
|
|
64
|
+
* const builder = new QueryBuilder(client)
|
|
65
|
+
* builder.withMetadata(true)
|
|
66
|
+
*/
|
|
67
|
+
withMetadata(withMetadata: boolean = true) {
|
|
68
|
+
this._withMetadata = withMetadata
|
|
69
|
+
return this
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Sets the withPayload flag which will return the payload for the entities if true
|
|
74
|
+
* @param withPayload - The boolean value to set
|
|
75
|
+
* @returns The QueryBuilder instance
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* const builder = new QueryBuilder(client)
|
|
79
|
+
* builder.withPayload(true)
|
|
80
|
+
*/
|
|
81
|
+
withPayload(withPayload: boolean = true) {
|
|
82
|
+
this._withPayload = withPayload
|
|
83
|
+
return this
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Sets the limit for the query
|
|
88
|
+
* @param limit - The number of entities to return
|
|
89
|
+
* @returns The QueryBuilder instance
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* const builder = new QueryBuilder(client)
|
|
93
|
+
* builder.limit(10)
|
|
94
|
+
*/
|
|
95
|
+
limit(limit: number) {
|
|
96
|
+
this._limit = limit
|
|
97
|
+
return this
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Sets the cursor for the query - it is advances setting which rather shouldn't be used manually but it is provided from query result if limit is used (pagination).
|
|
102
|
+
* @param cursor - The cursor to set which tells to RPC Query server where to start or continue the query.
|
|
103
|
+
* @returns The QueryBuilder instance
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* const builder = new QueryBuilder(client)
|
|
107
|
+
* builder.offset(10)
|
|
108
|
+
*/
|
|
109
|
+
cursor(cursor: string) {
|
|
110
|
+
this._cursor = cursor
|
|
111
|
+
return this
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Sets the validAtBlock for the query which tells at which block height the state we are intested.
|
|
116
|
+
* If not set, the latest block is used.
|
|
117
|
+
* @param validAtBlock - The block number to set
|
|
118
|
+
* @returns The QueryBuilder instance
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* const builder = new QueryBuilder(client)
|
|
122
|
+
* builder.validAtBlock(10000)
|
|
123
|
+
*/
|
|
124
|
+
validAtBlock(validAtBlock: bigint) {
|
|
125
|
+
this._validAtBlock = validAtBlock
|
|
126
|
+
return this
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Sets the predicates for the query limiting the results. It can be a single predicate or an array of predicates combined with 'and'.
|
|
131
|
+
* Predicates can be nested using 'or' and 'and' predicates.
|
|
132
|
+
* @param predicates - The predicates to set
|
|
133
|
+
* @returns The QueryBuilder instance
|
|
134
|
+
*
|
|
135
|
+
* @example
|
|
136
|
+
* const builder = new QueryBuilder(client)
|
|
137
|
+
* builder.where(eq("name", "John"))
|
|
138
|
+
* builder.where([eq("name", "John"), eq("age", 30)])
|
|
139
|
+
* builder.where([eq("name", "John"), or([eq("age", 30), eq("age", 31)])])
|
|
140
|
+
* builder.where([eq("name", "John"), and([eq("age", 30), eq("age", 31)])])
|
|
141
|
+
* builder.where([eq("name", "John"), or([eq("age", 30), and([eq("age", 31), eq("age", 32)])])])
|
|
142
|
+
* builder.where([eq("name", "John"), and([eq("age", 30), or([eq("age", 31), eq("age", 32)])])])
|
|
143
|
+
* builder.where([eq("name", "John"), and([eq("age", 30), or([eq("age", 31), and([eq("age", 32), eq("age", 33)])])])])
|
|
144
|
+
*/
|
|
145
|
+
where(predicates: Predicate[] | Predicate) {
|
|
146
|
+
if (Array.isArray(predicates)) {
|
|
147
|
+
this._predicates.push(...predicates)
|
|
148
|
+
} else {
|
|
149
|
+
this._predicates.push(predicates)
|
|
150
|
+
}
|
|
151
|
+
return this
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Fetches the entities from the query. Re
|
|
156
|
+
* It will return a QueryResult instance which can be used to fetch the next and previous pages.
|
|
157
|
+
* @returns The QueryResult instance {@link QueryResult}
|
|
158
|
+
*
|
|
159
|
+
* @example
|
|
160
|
+
* const builder = new QueryBuilder(client)
|
|
161
|
+
* const result = await builder.where(eq("name", "John")).fetch()
|
|
162
|
+
* // result = { entities: [Entity, Entity, Entity], next: async () => QueryResult, previous: async () => QueryResult }
|
|
163
|
+
*/
|
|
164
|
+
async fetch() {
|
|
165
|
+
const queryResult = await processQuery(this._client, {
|
|
166
|
+
predicates: this._predicates,
|
|
167
|
+
limit: this._limit,
|
|
168
|
+
cursor: this._cursor,
|
|
169
|
+
ownedBy: this._ownedBy,
|
|
170
|
+
validAtBlock: this._validAtBlock,
|
|
171
|
+
withAttributes: this._withAttributes,
|
|
172
|
+
withMetadata: this._withMetadata,
|
|
173
|
+
withPayload: this._withPayload,
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const entities = await Promise.all(
|
|
177
|
+
queryResult.data.map((entity) => entityFromRpcResult(entity)),
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
this.cursor(queryResult.cursor)
|
|
181
|
+
this.validAtBlock(queryResult.blockNumber)
|
|
182
|
+
|
|
183
|
+
return new QueryResult(entities, this, this._cursor, this._limit, this._validAtBlock)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Counts the entities from the query.
|
|
188
|
+
* @returns The number of entities
|
|
189
|
+
*
|
|
190
|
+
* @example
|
|
191
|
+
* const builder = new QueryBuilder(client)
|
|
192
|
+
* const result = await builder.where(eq("name", "John")).count()
|
|
193
|
+
* // result = 10
|
|
194
|
+
*/
|
|
195
|
+
async count() {
|
|
196
|
+
const queryResult = await processQuery(this._client, {
|
|
197
|
+
predicates: this._predicates,
|
|
198
|
+
limit: this._limit,
|
|
199
|
+
cursor: this._cursor,
|
|
200
|
+
ownedBy: this._ownedBy,
|
|
201
|
+
validAtBlock: this._validAtBlock,
|
|
202
|
+
withAttributes: false,
|
|
203
|
+
withMetadata: false,
|
|
204
|
+
withPayload: false,
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
return queryResult.data.length ?? 0
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, jest, test } from "bun:test"
|
|
2
|
+
import { Entity } from "../types/entity"
|
|
3
|
+
import { NoCursorOrLimitError, NoMoreResultsError } from "./errors"
|
|
4
|
+
import type { QueryBuilder } from "./queryBuilder"
|
|
5
|
+
import { QueryResult } from "./queryResult"
|
|
6
|
+
|
|
7
|
+
describe("QueryResult", () => {
|
|
8
|
+
let mockQueryBuilder: QueryBuilder
|
|
9
|
+
let mockEntities: Entity[]
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
// Create mock entities
|
|
13
|
+
mockEntities = [
|
|
14
|
+
new Entity("0x1" as const, "0xowner1" as const, 1000n, new Uint8Array([1]), []),
|
|
15
|
+
new Entity("0x2" as const, "0xowner2" as const, 2000n, new Uint8Array([2]), []),
|
|
16
|
+
new Entity("0x3" as const, "0xowner3" as const, 3000n, new Uint8Array([3]), []),
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
// Create mock query builder
|
|
20
|
+
mockQueryBuilder = {
|
|
21
|
+
cursor: jest.fn().mockReturnThis(),
|
|
22
|
+
fetch: jest.fn(),
|
|
23
|
+
} as unknown as QueryBuilder
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
describe("constructor", () => {
|
|
27
|
+
test("creates QueryResult with entities and query builder", () => {
|
|
28
|
+
const result = new QueryResult(
|
|
29
|
+
mockEntities,
|
|
30
|
+
mockQueryBuilder,
|
|
31
|
+
undefined,
|
|
32
|
+
undefined,
|
|
33
|
+
undefined,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
expect(result.entities).toEqual(mockEntities)
|
|
37
|
+
expect(result.entities).toHaveLength(3)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
test("creates QueryResult with limit and cursor", () => {
|
|
41
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, "0xABC123", 10, undefined)
|
|
42
|
+
|
|
43
|
+
expect(result.entities).toEqual(mockEntities)
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
test("creates QueryResult with undefined limit and cursor", () => {
|
|
47
|
+
const result = new QueryResult(
|
|
48
|
+
mockEntities,
|
|
49
|
+
mockQueryBuilder,
|
|
50
|
+
undefined,
|
|
51
|
+
undefined,
|
|
52
|
+
undefined,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
expect(result.entities).toEqual(mockEntities)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test("creates QueryResult with empty entities array", () => {
|
|
59
|
+
const result = new QueryResult([], mockQueryBuilder, "0xABC123", 10, undefined)
|
|
60
|
+
|
|
61
|
+
expect(result.entities).toEqual([])
|
|
62
|
+
expect(result.entities).toHaveLength(0)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
describe("next()", () => {
|
|
67
|
+
test("throws NoCursorOrLimitError when cursor is undefined", async () => {
|
|
68
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, undefined, 10, undefined)
|
|
69
|
+
|
|
70
|
+
await expect(result.next()).rejects.toThrow(NoCursorOrLimitError)
|
|
71
|
+
await expect(result.next()).rejects.toThrow("Cursor and limit must be defined to fetch next")
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test("throws NoCursorOrLimitError when limit is undefined", async () => {
|
|
75
|
+
const result = new QueryResult(
|
|
76
|
+
mockEntities,
|
|
77
|
+
mockQueryBuilder,
|
|
78
|
+
"0xABC123",
|
|
79
|
+
undefined,
|
|
80
|
+
undefined,
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
await expect(result.next()).rejects.toThrow(NoCursorOrLimitError)
|
|
84
|
+
await expect(result.next()).rejects.toThrow("Cursor and limit must be defined to fetch next")
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
test("throws NoCursorOrLimitError when both cursor and limit are undefined", async () => {
|
|
88
|
+
const result = new QueryResult(
|
|
89
|
+
mockEntities,
|
|
90
|
+
mockQueryBuilder,
|
|
91
|
+
undefined,
|
|
92
|
+
undefined,
|
|
93
|
+
undefined,
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
await expect(result.next()).rejects.toThrow(NoCursorOrLimitError)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test("throws NoMoreResultsError when limit > entities.length", async () => {
|
|
100
|
+
// limit is 4 but we have 3 entities, so limit > entities.length triggers error
|
|
101
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, "0xABC123", 4, 123n)
|
|
102
|
+
|
|
103
|
+
await expect(result.next()).rejects.toThrow(NoMoreResultsError)
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
test("fetches next page when limit equals entities length", async () => {
|
|
107
|
+
const newEntities = [
|
|
108
|
+
new Entity("0x4" as const, "0xowner4" as const, 4000n, new Uint8Array([4]), []),
|
|
109
|
+
new Entity("0x5" as const, "0xowner5" as const, 5000n, new Uint8Array([5]), []),
|
|
110
|
+
new Entity("0x6" as const, "0xowner6" as const, 6000n, new Uint8Array([6]), []),
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
const mockFetch = jest.fn().mockResolvedValue({
|
|
114
|
+
entities: newEntities,
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
mockQueryBuilder.fetch = mockFetch
|
|
118
|
+
mockQueryBuilder.cursor = jest.fn().mockReturnValue(mockQueryBuilder)
|
|
119
|
+
|
|
120
|
+
// limit is 3 and we have 3 entities, so there might be more
|
|
121
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, "0xABC123", 3, undefined)
|
|
122
|
+
|
|
123
|
+
await result.next()
|
|
124
|
+
|
|
125
|
+
expect(mockQueryBuilder.cursor).toHaveBeenCalledWith("0xABC123")
|
|
126
|
+
expect(mockFetch).toHaveBeenCalled()
|
|
127
|
+
expect(result.entities).toEqual(newEntities)
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
test("updates offset correctly for multiple next calls", async () => {
|
|
131
|
+
const initialEntities = [
|
|
132
|
+
new Entity("0x1" as const, "0xowner1" as const, 1000n, new Uint8Array([1]), []),
|
|
133
|
+
new Entity("0x2" as const, "0xowner2" as const, 2000n, new Uint8Array([2]), []),
|
|
134
|
+
new Entity("0x3" as const, "0xowner3" as const, 3000n, new Uint8Array([3]), []),
|
|
135
|
+
]
|
|
136
|
+
|
|
137
|
+
const newEntities1 = [
|
|
138
|
+
new Entity("0x4" as const, "0xowner4" as const, 4000n, new Uint8Array([4]), []),
|
|
139
|
+
new Entity("0x5" as const, "0xowner5" as const, 5000n, new Uint8Array([5]), []),
|
|
140
|
+
new Entity("0x6" as const, "0xowner6" as const, 6000n, new Uint8Array([6]), []),
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
const newEntities2 = [
|
|
144
|
+
new Entity("0x7" as const, "0xowner7" as const, 7000n, new Uint8Array([7]), []),
|
|
145
|
+
new Entity("0x8" as const, "0xowner8" as const, 8000n, new Uint8Array([8]), []),
|
|
146
|
+
new Entity("0x9" as const, "0xowner9" as const, 9000n, new Uint8Array([9]), []),
|
|
147
|
+
]
|
|
148
|
+
|
|
149
|
+
const mockCursor = jest.fn().mockReturnValue(mockQueryBuilder)
|
|
150
|
+
const mockFetch = jest
|
|
151
|
+
.fn()
|
|
152
|
+
.mockResolvedValueOnce({
|
|
153
|
+
entities: newEntities1,
|
|
154
|
+
queryBuilder: mockQueryBuilder,
|
|
155
|
+
cursor: "0xABC123",
|
|
156
|
+
})
|
|
157
|
+
.mockResolvedValueOnce({
|
|
158
|
+
entities: newEntities2,
|
|
159
|
+
queryBuilder: mockQueryBuilder,
|
|
160
|
+
cursor: "0xABC123",
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
mockQueryBuilder.cursor = mockCursor
|
|
164
|
+
mockQueryBuilder.fetch = mockFetch
|
|
165
|
+
|
|
166
|
+
// limit = 3, entities.length = 3, so 3 < 3 is false (won't throw)
|
|
167
|
+
const result = new QueryResult(initialEntities, mockQueryBuilder, "0xABC123", 3, undefined)
|
|
168
|
+
|
|
169
|
+
// First next call - offset should be 0 + 3 = 3
|
|
170
|
+
await result.next()
|
|
171
|
+
expect(mockCursor).toHaveBeenCalledWith("0xABC123")
|
|
172
|
+
expect(result.entities).toEqual(newEntities1)
|
|
173
|
+
|
|
174
|
+
// Second next call - offset should be 3 + 3 = 6
|
|
175
|
+
await result.next()
|
|
176
|
+
expect(mockCursor).toHaveBeenCalledWith("0xABC123")
|
|
177
|
+
expect(result.entities).toEqual(newEntities2)
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
test("fetches next page when limit is greater than entities length", async () => {
|
|
181
|
+
// limit is 5 but only 3 entities (5 > 3) so there is no more results
|
|
182
|
+
// so NoMoreResultsError is thrown
|
|
183
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, "0xABC123", 5, undefined)
|
|
184
|
+
|
|
185
|
+
await expect(result.next()).rejects.toThrow(NoMoreResultsError)
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
describe("pagination flow", () => {
|
|
190
|
+
test("simulates complete pagination: next -> next -> previous -> previous", async () => {
|
|
191
|
+
const page1 = [
|
|
192
|
+
new Entity("0x1" as const, "0xowner1" as const, 1000n, new Uint8Array([1]), []),
|
|
193
|
+
]
|
|
194
|
+
const page2 = [
|
|
195
|
+
new Entity("0x2" as const, "0xowner2" as const, 2000n, new Uint8Array([2]), []),
|
|
196
|
+
]
|
|
197
|
+
const page3 = [
|
|
198
|
+
new Entity("0x3" as const, "0xowner3" as const, 3000n, new Uint8Array([3]), []),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
const mockCursor = jest.fn().mockReturnValue(mockQueryBuilder)
|
|
202
|
+
const mockFetch = jest
|
|
203
|
+
.fn()
|
|
204
|
+
.mockResolvedValueOnce({
|
|
205
|
+
entities: page2,
|
|
206
|
+
queryBuilder: mockQueryBuilder,
|
|
207
|
+
cursor: "0xABC123",
|
|
208
|
+
}) // first next
|
|
209
|
+
.mockResolvedValueOnce({
|
|
210
|
+
entities: page3,
|
|
211
|
+
queryBuilder: mockQueryBuilder,
|
|
212
|
+
cursor: "0xABC123",
|
|
213
|
+
}) // second next
|
|
214
|
+
.mockResolvedValueOnce({
|
|
215
|
+
entities: page2,
|
|
216
|
+
queryBuilder: mockQueryBuilder,
|
|
217
|
+
cursor: "0xABC123",
|
|
218
|
+
}) // first previous
|
|
219
|
+
.mockResolvedValueOnce({
|
|
220
|
+
entities: page1,
|
|
221
|
+
queryBuilder: mockQueryBuilder,
|
|
222
|
+
cursor: "0xABC123",
|
|
223
|
+
}) // second previous
|
|
224
|
+
|
|
225
|
+
mockQueryBuilder.cursor = mockCursor
|
|
226
|
+
mockQueryBuilder.fetch = mockFetch
|
|
227
|
+
|
|
228
|
+
const result = new QueryResult(page1, mockQueryBuilder, "0xABC123", 1, undefined)
|
|
229
|
+
|
|
230
|
+
// Start at page 1 (offset 0)
|
|
231
|
+
expect(result.entities).toEqual(page1)
|
|
232
|
+
|
|
233
|
+
// Next to page 2 (offset 1)
|
|
234
|
+
await result.next()
|
|
235
|
+
expect(mockCursor).toHaveBeenCalledWith("0xABC123")
|
|
236
|
+
expect(result.entities).toEqual(page2)
|
|
237
|
+
|
|
238
|
+
// Next to page 3 (offset 2)
|
|
239
|
+
await result.next()
|
|
240
|
+
expect(mockCursor).toHaveBeenCalledWith("0xABC123")
|
|
241
|
+
expect(result.entities).toEqual(page3)
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
test("There is another page to fetch when limit < entities.length", async () => {
|
|
245
|
+
// limit is 2 but 3 entities, so 2 < 3 there is another page to fetch
|
|
246
|
+
const mockCursor = jest.fn().mockReturnValue(mockQueryBuilder)
|
|
247
|
+
const mockFetch = jest
|
|
248
|
+
.fn()
|
|
249
|
+
.mockResolvedValueOnce({
|
|
250
|
+
entities: mockEntities,
|
|
251
|
+
queryBuilder: mockQueryBuilder,
|
|
252
|
+
cursor: "0xABC123",
|
|
253
|
+
})
|
|
254
|
+
.mockResolvedValueOnce({
|
|
255
|
+
entities: mockEntities,
|
|
256
|
+
queryBuilder: mockQueryBuilder,
|
|
257
|
+
cursor: "0xABC123",
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
mockQueryBuilder.cursor = mockCursor
|
|
261
|
+
mockQueryBuilder.fetch = mockFetch
|
|
262
|
+
|
|
263
|
+
const result = new QueryResult(mockEntities, mockQueryBuilder, "0xABC123", 2, undefined)
|
|
264
|
+
|
|
265
|
+
await result.next()
|
|
266
|
+
expect(mockCursor).toHaveBeenCalledWith("0xABC123")
|
|
267
|
+
expect(result.entities).toEqual(mockEntities)
|
|
268
|
+
})
|
|
269
|
+
})
|
|
270
|
+
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { Entity } from "../types/entity"
|
|
2
|
+
import { NoCursorOrLimitError, NoMoreResultsError } from "./errors"
|
|
3
|
+
import type { QueryBuilder } from "./queryBuilder"
|
|
4
|
+
|
|
5
|
+
export class QueryResult {
|
|
6
|
+
entities: Entity[]
|
|
7
|
+
private _endOfIteration: boolean
|
|
8
|
+
private _cursor: string | undefined
|
|
9
|
+
private _limit: number | undefined
|
|
10
|
+
private _validAtBlock: bigint | undefined
|
|
11
|
+
private _queryBuilder: QueryBuilder
|
|
12
|
+
|
|
13
|
+
// Public getters for internal state
|
|
14
|
+
get queryBuilder(): QueryBuilder {
|
|
15
|
+
return this._queryBuilder
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
get cursor(): string | undefined {
|
|
19
|
+
return this._cursor
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
constructor(
|
|
23
|
+
entities: Entity[],
|
|
24
|
+
queryBuilder: QueryBuilder,
|
|
25
|
+
cursor: string | undefined,
|
|
26
|
+
limit: number | undefined,
|
|
27
|
+
validAtBlock: bigint | undefined,
|
|
28
|
+
) {
|
|
29
|
+
this.entities = entities
|
|
30
|
+
this._queryBuilder = queryBuilder
|
|
31
|
+
this._endOfIteration = !limit || entities.length < limit
|
|
32
|
+
this._cursor = cursor
|
|
33
|
+
this._limit = limit
|
|
34
|
+
this._validAtBlock = validAtBlock
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async next() {
|
|
38
|
+
if (this._cursor === undefined || this._limit === undefined) {
|
|
39
|
+
throw new NoCursorOrLimitError()
|
|
40
|
+
}
|
|
41
|
+
if (this._endOfIteration) {
|
|
42
|
+
throw new NoMoreResultsError()
|
|
43
|
+
}
|
|
44
|
+
this._queryBuilder.cursor(this._cursor)
|
|
45
|
+
const result = await this._queryBuilder.fetch()
|
|
46
|
+
this.entities = result.entities
|
|
47
|
+
// Update the query builder reference
|
|
48
|
+
this._queryBuilder = result.queryBuilder
|
|
49
|
+
// Check if we've reached the end (no more cursor or we got fewer entities than limit)
|
|
50
|
+
this._endOfIteration = !result.cursor || result.entities.length < this._limit
|
|
51
|
+
// Update the cursor
|
|
52
|
+
this._cursor = result.cursor
|
|
53
|
+
|
|
54
|
+
// TODO check current block height and if it is not too old
|
|
55
|
+
console.debug("Current block height for next page: ", this._validAtBlock)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hasNextPage() {
|
|
59
|
+
return !this._endOfIteration
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { bytesToString, type Hex } from "viem"
|
|
2
|
+
import type { MimeType } from "../types"
|
|
3
|
+
import type { Attribute } from "./attributes"
|
|
4
|
+
|
|
5
|
+
export class Entity {
|
|
6
|
+
key: Hex
|
|
7
|
+
owner: Hex
|
|
8
|
+
expiresAtBlock: bigint
|
|
9
|
+
payload: Uint8Array
|
|
10
|
+
attributes: Attribute[]
|
|
11
|
+
contentType: MimeType
|
|
12
|
+
|
|
13
|
+
constructor(
|
|
14
|
+
key: Hex,
|
|
15
|
+
contentType: MimeType,
|
|
16
|
+
owner: Hex,
|
|
17
|
+
expiresAtBlock: bigint,
|
|
18
|
+
payload: Uint8Array,
|
|
19
|
+
attributes: Attribute[],
|
|
20
|
+
) {
|
|
21
|
+
this.key = key
|
|
22
|
+
this.owner = owner
|
|
23
|
+
this.expiresAtBlock = expiresAtBlock
|
|
24
|
+
this.payload = payload
|
|
25
|
+
this.attributes = attributes
|
|
26
|
+
this.contentType = contentType
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
toText(): string {
|
|
30
|
+
return bytesToString(this.payload)
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
toJson(): any {
|
|
34
|
+
return JSON.parse(bytesToString(this.payload))
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Hex } from "viem"
|
|
2
|
+
|
|
3
|
+
export type OnEntityCreatedEvent = {
|
|
4
|
+
entityKey: Hex
|
|
5
|
+
owner: Hex
|
|
6
|
+
expirationBlock: number
|
|
7
|
+
cost: bigint
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export type OnEntityUpdatedEvent = {
|
|
11
|
+
entityKey: Hex
|
|
12
|
+
owner: Hex
|
|
13
|
+
oldExpirationBlock: number
|
|
14
|
+
newExpirationBlock: number
|
|
15
|
+
cost: bigint
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export type OnEntityDeletedEvent = {
|
|
19
|
+
entityKey: Hex
|
|
20
|
+
owner: Hex
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type OnEntityExpiredEvent = {
|
|
24
|
+
entityKey: Hex
|
|
25
|
+
owner: Hex
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export type OnEntityExpiresInExtendedEvent = {
|
|
29
|
+
entityKey: Hex
|
|
30
|
+
owner: Hex
|
|
31
|
+
oldExpirationBlock: number
|
|
32
|
+
newExpirationBlock: number
|
|
33
|
+
cost: bigint
|
|
34
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export type MimeType =
|
|
2
|
+
| "text/plain"
|
|
3
|
+
| "text/html"
|
|
4
|
+
| "text/css"
|
|
5
|
+
| "text/csv"
|
|
6
|
+
| "text/xml"
|
|
7
|
+
| "text/javascript"
|
|
8
|
+
| "application/json"
|
|
9
|
+
| "application/xml"
|
|
10
|
+
| "application/pdf"
|
|
11
|
+
| "application/zip"
|
|
12
|
+
| "application/gzip"
|
|
13
|
+
| "application/octet-stream"
|
|
14
|
+
| "application/javascript"
|
|
15
|
+
| "application/x-www-form-urlencoded"
|
|
16
|
+
| "image/png"
|
|
17
|
+
| "image/jpeg"
|
|
18
|
+
| "image/webp"
|
|
19
|
+
| "image/gif"
|
|
20
|
+
| "image/svg+xml"
|
|
21
|
+
| "audio/mpeg"
|
|
22
|
+
| "audio/wav"
|
|
23
|
+
| "audio/ogg"
|
|
24
|
+
| "video/mp4"
|
|
25
|
+
| "video/webm"
|
|
26
|
+
| "video/ogg"
|
|
27
|
+
| "multipart/form-data"
|