@onchaindb/sdk 0.4.4 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (97) hide show
  1. package/.claude/settings.local.json +10 -2
  2. package/README.md +422 -355
  3. package/dist/batch.d.ts +1 -10
  4. package/dist/batch.d.ts.map +1 -1
  5. package/dist/batch.js +4 -26
  6. package/dist/batch.js.map +1 -1
  7. package/dist/client.d.ts +29 -43
  8. package/dist/client.d.ts.map +1 -1
  9. package/dist/client.js +199 -323
  10. package/dist/client.js.map +1 -1
  11. package/dist/database.d.ts +14 -131
  12. package/dist/database.d.ts.map +1 -1
  13. package/dist/database.js +35 -131
  14. package/dist/database.js.map +1 -1
  15. package/dist/index.d.ts +6 -9
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +1 -15
  18. package/dist/index.js.map +1 -1
  19. package/dist/query-sdk/ConditionBuilder.d.ts +3 -11
  20. package/dist/query-sdk/ConditionBuilder.d.ts.map +1 -1
  21. package/dist/query-sdk/ConditionBuilder.js +10 -48
  22. package/dist/query-sdk/ConditionBuilder.js.map +1 -1
  23. package/dist/query-sdk/NestedBuilders.d.ts +33 -30
  24. package/dist/query-sdk/NestedBuilders.d.ts.map +1 -1
  25. package/dist/query-sdk/NestedBuilders.js +46 -43
  26. package/dist/query-sdk/NestedBuilders.js.map +1 -1
  27. package/dist/query-sdk/QueryBuilder.d.ts +4 -2
  28. package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
  29. package/dist/query-sdk/QueryBuilder.js +47 -169
  30. package/dist/query-sdk/QueryBuilder.js.map +1 -1
  31. package/dist/query-sdk/QueryResult.d.ts +0 -38
  32. package/dist/query-sdk/QueryResult.d.ts.map +1 -1
  33. package/dist/query-sdk/QueryResult.js +1 -227
  34. package/dist/query-sdk/QueryResult.js.map +1 -1
  35. package/dist/query-sdk/index.d.ts +1 -1
  36. package/dist/query-sdk/index.d.ts.map +1 -1
  37. package/dist/query-sdk/index.js.map +1 -1
  38. package/dist/query-sdk/operators.d.ts +32 -28
  39. package/dist/query-sdk/operators.d.ts.map +1 -1
  40. package/dist/query-sdk/operators.js +45 -155
  41. package/dist/query-sdk/operators.js.map +1 -1
  42. package/dist/types.d.ts +153 -1
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/jest.config.js +4 -0
  46. package/package.json +1 -1
  47. package/skills.md +0 -1
  48. package/src/client.ts +243 -745
  49. package/src/database.ts +70 -493
  50. package/src/index.ts +40 -193
  51. package/src/query-sdk/ConditionBuilder.ts +37 -89
  52. package/src/query-sdk/NestedBuilders.ts +90 -92
  53. package/src/query-sdk/QueryBuilder.ts +59 -218
  54. package/src/query-sdk/QueryResult.ts +4 -330
  55. package/src/query-sdk/README.md +214 -583
  56. package/src/query-sdk/index.ts +1 -1
  57. package/src/query-sdk/operators.ts +91 -200
  58. package/src/query-sdk/tests/FieldConditionBuilder.test.ts +70 -71
  59. package/src/query-sdk/tests/LogicalOperator.test.ts +43 -82
  60. package/src/query-sdk/tests/NestedBuilders.test.ts +229 -309
  61. package/src/query-sdk/tests/QueryBuilder.test.ts +5 -5
  62. package/src/query-sdk/tests/QueryResult.test.ts +41 -435
  63. package/src/query-sdk/tests/comprehensive.test.ts +4 -185
  64. package/src/tests/client-requests.test.ts +280 -0
  65. package/src/tests/client-validation.test.ts +80 -0
  66. package/src/types.ts +229 -8
  67. package/src/batch.ts +0 -257
  68. package/src/query-sdk/dist/ConditionBuilder.d.ts +0 -22
  69. package/src/query-sdk/dist/ConditionBuilder.js +0 -90
  70. package/src/query-sdk/dist/FieldConditionBuilder.d.ts +0 -1
  71. package/src/query-sdk/dist/FieldConditionBuilder.js +0 -6
  72. package/src/query-sdk/dist/NestedBuilders.d.ts +0 -43
  73. package/src/query-sdk/dist/NestedBuilders.js +0 -144
  74. package/src/query-sdk/dist/OnChainDB.d.ts +0 -19
  75. package/src/query-sdk/dist/OnChainDB.js +0 -123
  76. package/src/query-sdk/dist/QueryBuilder.d.ts +0 -70
  77. package/src/query-sdk/dist/QueryBuilder.js +0 -295
  78. package/src/query-sdk/dist/QueryResult.d.ts +0 -52
  79. package/src/query-sdk/dist/QueryResult.js +0 -293
  80. package/src/query-sdk/dist/SelectionBuilder.d.ts +0 -20
  81. package/src/query-sdk/dist/SelectionBuilder.js +0 -80
  82. package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +0 -27
  83. package/src/query-sdk/dist/adapters/HttpClientAdapter.js +0 -170
  84. package/src/query-sdk/dist/index.d.ts +0 -36
  85. package/src/query-sdk/dist/index.js +0 -27
  86. package/src/query-sdk/dist/operators.d.ts +0 -56
  87. package/src/query-sdk/dist/operators.js +0 -289
  88. package/src/query-sdk/dist/tests/setup.d.ts +0 -15
  89. package/src/query-sdk/dist/tests/setup.js +0 -46
  90. package/src/query-sdk/jest.config.js +0 -25
  91. package/src/query-sdk/package.json +0 -46
  92. package/src/query-sdk/tests/aggregations.test.ts +0 -653
  93. package/src/query-sdk/tests/integration.test.ts +0 -608
  94. package/src/query-sdk/tests/operators.test.ts +0 -327
  95. package/src/query-sdk/tests/unit.test.ts +0 -794
  96. package/src/query-sdk/tsconfig.json +0 -26
  97. package/src/query-sdk/yarn.lock +0 -3092
package/README.md CHANGED
@@ -1,13 +1,11 @@
1
1
  # OnChainDB TypeScript SDK
2
2
 
3
- A TypeScript SDK for OnChainDB - the decentralized database built on Celestia blockchain with x402 payment protocol integration.
3
+ A TypeScript SDK for OnChainDB the decentralized database built on Celestia with x402 payment protocol integration.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
8
  npm install @onchaindb/sdk
9
- # or
10
- yarn add @onchaindb/sdk
11
9
  ```
12
10
 
13
11
  ## Quick Start
@@ -18,7 +16,7 @@ import { createClient } from '@onchaindb/sdk';
18
16
  const db = createClient({
19
17
  endpoint: 'https://api.onchaindb.io',
20
18
  appId: 'my_app',
21
- apiKey: 'your_api_key'
19
+ appKey: 'your_app_key'
22
20
  });
23
21
 
24
22
  // Store data
@@ -42,20 +40,34 @@ console.log(result.records);
42
40
 
43
41
  ```typescript
44
42
  interface OnChainDBConfig {
45
- endpoint: string; // OnChainDB server endpoint
46
- appId?: string; // Application ID
47
- apiKey?: string; // API key for authentication
48
- timeout?: number; // Request timeout (default: 30000ms)
49
- retryCount?: number; // Retry attempts (default: 3)
50
- retryDelay?: number; // Retry delay (default: 1000ms)
43
+ endpoint: string; // OnChainDB server endpoint
44
+ appId?: string; // Application ID
45
+ appKey?: string; // App key for write operations (X-App-Key header)
46
+ userKey?: string; // User key for Auto-Pay (X-User-Key header)
47
+ agentKey?: string; // Agent key with Pay permission (X-Agent-Key header)
48
+ timeout?: number; // Request timeout in ms (default: 30000)
49
+ retryCount?: number; // Retry attempts (default: 3)
50
+ retryDelay?: number; // Retry delay in ms (default: 1000)
51
51
  }
52
52
  ```
53
53
 
54
+ ### Agent Keys
55
+
56
+ Keys with the `Pay` permission are Agent Keys — they can pay other apps inline using USDC via EIP-3009.
57
+
58
+ ```typescript
59
+ const db = createClient({
60
+ endpoint: 'https://api.onchaindb.io',
61
+ appId: 'my_app',
62
+ agentKey: 'agent_key_with_pay_permission'
63
+ });
64
+ ```
65
+
54
66
  ## Storing Data
55
67
 
56
68
  ```typescript
57
69
  // Basic store
58
- const result = await db.store({
70
+ await db.store({
59
71
  collection: 'tweets',
60
72
  data: [
61
73
  { id: 'tweet_1', content: 'Hello!', author: 'alice' },
@@ -63,24 +75,29 @@ const result = await db.store({
63
75
  ]
64
76
  });
65
77
 
66
- // Store with payment (x402 protocol)
67
- const result = await db.store(
78
+ // Store with x402 payment callback
79
+ await db.store(
68
80
  { collection: 'tweets', data: [{ content: 'Paid tweet' }] },
69
81
  async (quote) => {
70
- // Pay via wallet (e.g., Keplr)
71
- const txHash = await wallet.signAndBroadcast(
72
- quote.broker_address,
73
- `${quote.total_cost_utia}utia`,
74
- 'OnChainDB storage'
75
- );
76
- return { txHash, network: 'mocha-4' };
82
+ // quote is an X402Quote — sign and broadcast payment via your wallet
83
+ const txHash = await wallet.sendTransaction({
84
+ to: quote.payTo,
85
+ value: quote.maxAmountRequired
86
+ });
87
+ return {
88
+ txHash,
89
+ network: quote.network,
90
+ sender: wallet.address,
91
+ chainType: quote.chainType,
92
+ paymentMethod: quote.paymentMethod
93
+ };
77
94
  }
78
95
  );
79
96
  ```
80
97
 
81
98
  ## Query Builder
82
99
 
83
- The SDK provides a fluent query builder for constructing queries. For comprehensive query documentation including all operators, nested queries, and advanced patterns, see the [Query SDK Reference](./src/query-sdk/README.md).
100
+ The SDK provides a fluent query builder for constructing queries. See the [Query SDK Reference](./src/query-sdk/README.md) for the full operator and aggregation reference.
84
101
 
85
102
  ### Basic Queries
86
103
 
@@ -92,25 +109,35 @@ const result = await db.queryBuilder()
92
109
  .limit(50)
93
110
  .execute();
94
111
 
95
- // Access results - records have flat structure
96
- result.records.forEach(user => {
97
- console.log(user.name, user.email);
98
- });
112
+ result.records.forEach(user => console.log(user.name, user.email));
99
113
  ```
100
114
 
101
- ### Query Operators
115
+ ### All Field Operators
102
116
 
103
117
  ```typescript
104
- .equals(value)
105
- .notEquals(value)
106
- .greaterThan(value)
107
- .lessThan(value)
108
- .contains(value)
109
- .startsWith(value)
110
- .isNull()
111
- .isNotNull()
112
- .in(values)
113
- .notIn(values)
118
+ // Comparison
119
+ .equals(value) .notEquals(value)
120
+ .greaterThan(value) .greaterThanOrEqual(value)
121
+ .lessThan(value) .lessThanOrEqual(value)
122
+ .between(min, max)
123
+
124
+ // String
125
+ .contains(value) .startsWith(value) .endsWith(value)
126
+ .regExpMatches(pattern)
127
+ .includesCaseInsensitive(value)
128
+ .startsWithCaseInsensitive(value)
129
+ .endsWithCaseInsensitive(value)
130
+
131
+ // Array
132
+ .in(values) .notIn(values)
133
+
134
+ // Existence
135
+ .exists() .notExists() .isNull() .isNotNull()
136
+ .isTrue() .isFalse()
137
+
138
+ // Network / security
139
+ .isLocalIp() .isExternalIp() .inCountry(code) .cidr(range)
140
+ .b64(value) .inDataset(name)
114
141
  ```
115
142
 
116
143
  ### Complex Queries with Logical Operators
@@ -122,10 +149,13 @@ const result = await db.queryBuilder()
122
149
  .collection('posts')
123
150
  .find(builder =>
124
151
  LogicalOperator.And([
125
- LogicalOperator.Condition(builder.field('published').equals(true)),
152
+ builder.field('published').isTrue(),
126
153
  LogicalOperator.Or([
127
- LogicalOperator.Condition(builder.field('category').equals('tech')),
128
- LogicalOperator.Condition(builder.field('views').greaterThan(1000))
154
+ builder.field('category').equals('tech'),
155
+ builder.field('views').greaterThan(1000)
156
+ ]),
157
+ LogicalOperator.Not([
158
+ builder.field('archived').isTrue()
129
159
  ])
130
160
  ])
131
161
  )
@@ -134,124 +164,84 @@ const result = await db.queryBuilder()
134
164
  .execute();
135
165
  ```
136
166
 
137
- ## Server-Side JOINs
138
-
139
- Server-side JOINs execute on the backend for optimal performance. Use `$data.fieldname` to reference parent record fields.
140
-
141
- ### One-to-One JOIN (joinOne)
142
-
143
- Returns a single object or null.
167
+ ### Sorting & Pagination
144
168
 
145
169
  ```typescript
146
- // Get tweets with author profile
147
170
  const result = await db.queryBuilder()
148
- .collection('tweets')
149
- .joinOne('author_info', 'users')
150
- .onField('address').equals('$data.author')
151
- .selectFields(['address', 'display_name', 'avatar_url', 'verified'])
152
- .build()
153
- .selectAll()
171
+ .collection('posts')
172
+ .whereField('published').isTrue()
173
+ .orderBy('created_at', 'DESC')
154
174
  .limit(20)
175
+ .offset(40)
176
+ .selectAll()
155
177
  .execute();
156
-
157
- // Result structure:
158
- // {
159
- // id: 'tweet_123',
160
- // content: 'Hello world!',
161
- // author: 'celestia1abc...',
162
- // author_info: {
163
- // address: 'celestia1abc...',
164
- // display_name: 'Alice',
165
- // avatar_url: 'https://...',
166
- // verified: true
167
- // }
168
- // }
169
178
  ```
170
179
 
171
- ### One-to-Many JOIN (joinMany)
172
-
173
- Returns an array of related records.
180
+ ### Server-Side Aggregations
174
181
 
175
182
  ```typescript
176
- // Get users with their tweets
177
- const result = await db.queryBuilder()
183
+ // Count
184
+ const total = await db.queryBuilder()
178
185
  .collection('users')
179
- .whereField('address').equals(userAddress)
180
- .joinMany('user_tweets', 'tweets')
181
- .onField('author').equals('$data.address')
182
- .selectFields(['id', 'content', 'created_at'])
183
- .build()
184
- .selectAll()
185
- .execute();
186
+ .whereField('active').isTrue()
187
+ .count();
188
+
189
+ // Sum / average
190
+ const revenue = await db.queryBuilder().collection('orders').sumBy('amount');
191
+ const avg = await db.queryBuilder().collection('orders').avgBy('amount');
186
192
 
187
- // Result structure:
188
- // {
189
- // address: 'celestia1abc...',
190
- // display_name: 'Alice',
191
- // user_tweets: [
192
- // { id: 'tweet_1', content: 'Hello!', created_at: '...' },
193
- // { id: 'tweet_2', content: 'World!', created_at: '...' }
194
- // ]
195
- // }
193
+ // Group by
194
+ const byCountry = await db.queryBuilder()
195
+ .collection('users')
196
+ .groupBy('country')
197
+ .count();
198
+ // { "USA": 150, "UK": 75, ... }
196
199
  ```
197
200
 
198
- ### Multiple JOINs
201
+ ## Server-Side JOINs
202
+
203
+ JOINs execute on the backend in a single request. Use `$data.fieldname` to reference parent record fields.
204
+
205
+ ### joinOne (one-to-one)
199
206
 
200
207
  ```typescript
201
- // Timeline with likes, replies, and author info
202
208
  const result = await db.queryBuilder()
203
209
  .collection('tweets')
204
- .whereField('reply_to_id').isNull()
205
210
  .joinOne('author_info', 'users')
206
211
  .onField('address').equals('$data.author')
207
212
  .selectFields(['display_name', 'avatar_url', 'verified'])
208
213
  .build()
209
- .joinMany('likes', 'likes')
210
- .onField('tweet_id').equals('$data.id')
211
- .selectFields(['user', 'created_at'])
212
- .build()
213
- .joinMany('replies', 'tweets')
214
- .onField('reply_to_id').equals('$data.id')
215
- .selectFields(['id', 'author', 'content'])
216
- .build()
217
214
  .selectAll()
218
- .offset(offset)
219
- .limit(limit)
215
+ .limit(20)
220
216
  .execute();
217
+
218
+ // result.records[0].author_info => { display_name, avatar_url, verified } | null
221
219
  ```
222
220
 
223
- ### Nested JOINs
221
+ ### joinMany (one-to-many)
224
222
 
225
223
  ```typescript
226
- // Get tweet with replies, each reply includes author info
227
224
  const result = await db.queryBuilder()
228
- .collection('tweets')
229
- .whereField('id').equals(tweetId)
230
- .joinMany('replies', 'tweets')
231
- .onField('reply_to_id').equals('$data.id')
232
- .selectAll()
233
- // Nested: get author for each reply
234
- .joinOne('author_info', 'users')
235
- .onField('address').equals('$data.author')
236
- .selectFields(['display_name', 'avatar_url', 'verified'])
237
- .build()
225
+ .collection('users')
226
+ .whereField('address').equals(userAddress)
227
+ .joinMany('tweets', 'tweets')
228
+ .onField('author').equals('$data.address')
229
+ .selectFields(['id', 'content', 'created_at'])
238
230
  .build()
239
231
  .selectAll()
240
- .limit(1)
241
232
  .execute();
233
+
234
+ // result.records[0].tweets => [{ id, content, created_at }, ...]
242
235
  ```
243
236
 
244
- ### Self-Referential JOINs
237
+ ### Nested JOINs
245
238
 
246
239
  ```typescript
247
- // Get tweets with quoted tweet info
248
240
  const result = await db.queryBuilder()
249
241
  .collection('tweets')
250
- .whereField('quote_tweet_id').isNotNull()
251
- .joinOne('quote_tweet', 'tweets')
252
- .onField('id').equals('$data.quote_tweet_id')
253
- .selectFields(['id', 'content', 'author', 'created_at'])
254
- // Nested: get quoted tweet's author
242
+ .joinMany('replies', 'tweets')
243
+ .onField('reply_to_id').equals('$data.id')
244
+ .selectAll()
255
245
  .joinOne('author_info', 'users')
256
246
  .onField('address').equals('$data.author')
257
247
  .selectFields(['display_name', 'avatar_url'])
@@ -261,299 +251,331 @@ const result = await db.queryBuilder()
261
251
  .execute();
262
252
  ```
263
253
 
264
- ## Blob Storage
254
+ ## Collection Management
255
+
256
+ ### Create & Sync Collections
265
257
 
266
258
  ```typescript
267
- // Get pricing quote
268
- const quote = await db.getPricingQuote({
269
- app_id: 'my_app',
270
- operation_type: 'write',
271
- size_kb: Math.ceil(file.size / 1024),
272
- collection: 'images'
273
- });
259
+ import { SimpleCollectionSchema } from '@onchaindb/sdk';
274
260
 
275
- const costInUtia = Math.ceil(quote.total_cost * 1_000_000);
261
+ const schema: SimpleCollectionSchema = {
262
+ name: 'users',
263
+ fields: {
264
+ email: { type: 'string', index: true },
265
+ age: { type: 'number', index: true },
266
+ status: { type: 'string', index: true, indexType: 'hash' },
267
+ bio: { type: 'string', index: true, indexType: 'fulltext' },
268
+ 'address.city': { type: 'string', index: true },
269
+ },
270
+ useBaseFields: true, // Auto-index id, createdAt, updatedAt, deletedAt
271
+ };
276
272
 
277
- // Pay for storage
278
- const txHash = await wallet.signAndBroadcast(
279
- brokerAddress,
280
- `${costInUtia}utia`,
281
- 'Image upload'
282
- );
273
+ // Create collection with indexes
274
+ await db.createCollection(schema);
283
275
 
284
- // Upload blob
285
- const upload = await db.uploadBlob({
286
- collection: 'images',
287
- blob: file,
288
- metadata: { user_address: userAddress },
289
- payment_tx_hash: txHash,
290
- user_address: userAddress,
291
- broker_address: brokerAddress,
292
- amount_utia: costInUtia
293
- });
276
+ // Sync: creates new indexes, removes none (broker handles upsert)
277
+ await db.syncCollection(schema);
278
+ ```
294
279
 
295
- // Wait for completion
296
- const task = await db.waitForTaskCompletion(upload.ticket_id);
280
+ ### Sharding
297
281
 
298
- if (task.status === 'Completed') {
299
- console.log('Blob ID:', upload.blob_id);
300
- }
282
+ Partition large collections across multiple files for massive datasets.
301
283
 
302
- // Retrieve blob
303
- const blob = await db.retrieveBlob({
304
- collection: 'images',
305
- blob_id: upload.blob_id
306
- });
307
- ```
284
+ ```typescript
285
+ import { SimpleCollectionSchemaWithSharding, ShardingStrategy } from '@onchaindb/sdk';
308
286
 
309
- ## Task Tracking
287
+ const schema: SimpleCollectionSchemaWithSharding = {
288
+ name: 'orderbook_snapshots',
289
+ fields: {
290
+ market: { type: 'string', index: true },
291
+ timestamp: { type: 'number', index: true },
292
+ mid_price: { type: 'number' }
293
+ },
294
+ sharding: {
295
+ keys: [
296
+ { field: 'market', type: 'discrete' },
297
+ { field: 'timestamp', type: 'time_range', granularity: 'hour' }
298
+ ],
299
+ enforce_in_queries: true
300
+ }
301
+ };
310
302
 
311
- ```typescript
312
- // Check task status
313
- const task = await db.getTaskStatus(ticket_id);
314
- console.log('Status:', task.status);
315
- // Pending, PaymentBroadcast, PaymentConfirming, Completed, Failed
316
-
317
- // Wait with polling
318
- const completed = await db.waitForTaskCompletion(
319
- ticket_id,
320
- 2000, // Poll interval (ms)
321
- 600000 // Max wait (ms)
322
- );
303
+ // syncCollection applies sharding automatically if present
304
+ await db.syncCollection(schema);
305
+
306
+ // Or configure sharding separately
307
+ await db.setupSharding('orderbook_snapshots', schema.sharding!);
323
308
  ```
324
309
 
325
- ## Cost Estimation
310
+ ### Update & Delete Collections
326
311
 
327
312
  ```typescript
328
- const quote = await db.getPricingQuote({
329
- app_id: 'my_app',
330
- operation_type: 'write',
331
- size_kb: 50,
332
- collection: 'tweets'
313
+ // Update collection properties
314
+ await db.updateCollection('users', {
315
+ public: true,
316
+ description: 'User profiles',
317
+ default_sort_column: 'created_at',
318
+ collection_type: 'local' // 'local' | 'offline' | 'api' | 'hybrid'
333
319
  });
334
320
 
335
- console.log('Total cost:', quote.total_cost, 'TIA');
336
- console.log('In utia:', Math.ceil(quote.total_cost * 1_000_000));
337
- ```
321
+ // Delete a collection
322
+ await db.deleteCollection('old_collection');
338
323
 
339
- ## Collection Schema Management
324
+ // Get collection info by ID (ID format: "{appId}_{index}", from listCollections)
325
+ const info = await db.getCollectionInfo('myapp_0');
326
+ // { id, name, namespace, primary_column, sort_column, status, collection_type,
327
+ // document_count, size_kb, created_at, last_modified }
328
+ ```
340
329
 
341
- Define and manage collection indexes using a schema-first approach:
330
+ ## Data Retention
342
331
 
343
- ### Create Collection
332
+ Configure per-collection retention policies. First 30 days are always free.
344
333
 
345
334
  ```typescript
346
- import { SimpleCollectionSchema } from '@onchaindb/sdk';
335
+ // Get current retention config
336
+ const config = await db.getRetention('orders');
337
+ // { collection, retention_days, monthly_cost_per_kb, last_cleanup, status }
347
338
 
348
- const schema: SimpleCollectionSchema = {
349
- name: 'users',
350
- fields: {
351
- email: { type: 'string', index: true },
352
- age: { type: 'number', index: true },
353
- status: { type: 'string', index: true, indexType: 'hash' },
354
- bio: { type: 'string', index: true, indexType: 'fulltext' },
355
- 'address.city': { type: 'string', index: true }, // Nested field
356
- },
357
- useBaseFields: true, // Auto-index id, createdAt, updatedAt, deletedAt
358
- };
339
+ // Set retention (null = permanent storage)
340
+ const result = await db.setRetention('orders', { retention_days: 90 });
341
+ // { message, config: RetentionConfig }
359
342
 
360
- const result = await db.createCollection(schema);
361
- // { collection: 'users', indexes: [...], success: true, warnings: [] }
343
+ // Get cost estimate for all collections
344
+ const costs = await db.getRetentionCost();
345
+ // { app_id, collections: CollectionRetentionCost[], total_monthly_cost_tia, projected_next_month_cost_tia }
362
346
  ```
363
347
 
364
- ### Sync Collection
348
+ ## SQL Interface
365
349
 
366
- Update indexes to match schema (creates new, removes old):
350
+ Query and insert data using SQL syntax. Table references use `app::collection` format.
367
351
 
368
352
  ```typescript
369
- const updatedSchema: SimpleCollectionSchema = {
370
- name: 'users',
371
- fields: {
372
- email: { type: 'string', index: true },
373
- username: { type: 'string', index: true }, // New index
374
- // 'age' index removed
375
- },
376
- };
353
+ // SQL query
354
+ const result = await db.sql(
355
+ 'SELECT * FROM myapp::orders WHERE amount > 100 ORDER BY created_at DESC LIMIT 50'
356
+ );
357
+ // { data: any[], count, query, app_id, collection }
377
358
 
378
- const result = await db.syncCollection(updatedSchema);
379
- // { collection: 'users', created: [...], removed: [...], unchanged: [...], success: true }
380
- ```
359
+ // SQL query with history (includes all versions of records)
360
+ const withHistory = await db.sql(
361
+ 'SELECT * FROM myapp::orders',
362
+ { includeHistory: true }
363
+ );
381
364
 
382
- ### Field Types & Index Types
365
+ // SQL insert
366
+ await db.sqlInsert(
367
+ "INSERT INTO myapp::orders (id, amount, status) VALUES ('ord_1', 250, 'pending')"
368
+ );
369
+ ```
383
370
 
384
- | Field Type | Index Types |
385
- |------------|-------------|
386
- | `string` | `string` (default), `hash`, `fulltext` |
387
- | `number` | `number` (default) |
388
- | `boolean` | `boolean` (default) |
389
- | `date` | `date` (default) |
390
- | `object` | `string` |
391
- | `array` | `string` |
371
+ ## Predefined Queries
392
372
 
393
- ### Read Pricing on Fields
373
+ Named, parameterized queries that can be executed publicly without authentication.
394
374
 
395
375
  ```typescript
396
- const schema: SimpleCollectionSchema = {
397
- name: 'premium_content',
398
- fields: {
399
- title: { type: 'string', index: true },
400
- content: {
401
- type: 'string',
402
- index: true,
403
- readPricing: {
404
- pricePerAccess: 0.001, // 0.001 TIA per read
405
- },
376
+ import { CreateQueryRequest } from '@onchaindb/sdk';
377
+
378
+ // Create a predefined query
379
+ const created = await db.createQuery({
380
+ name: 'active_markets',
381
+ source_collection: 'orderbook_snapshots',
382
+ base_query: { find: { status: { $eq: 'active' } } },
383
+ parameters: [
384
+ {
385
+ name: 'market',
386
+ field_path: 'market', // maps to find.market.$eq
387
+ required: true,
388
+ description: 'Market symbol (e.g. BTC, ETH)'
406
389
  },
407
- },
408
- };
390
+ {
391
+ name: 'min_price',
392
+ field_path: 'price.$gte',
393
+ default: 0,
394
+ description: 'Minimum price filter'
395
+ }
396
+ ],
397
+ description: 'Query active orderbook snapshots by market'
398
+ });
399
+
400
+ // List all queries
401
+ const queries = await db.listQueries();
402
+ // QueryDefinition[]
403
+
404
+ // Get a specific query definition
405
+ const query = await db.getQuery('active_markets');
406
+
407
+ // Execute a predefined query (public endpoint — no auth required)
408
+ const data = await db.executeQuery(
409
+ 'active_markets',
410
+ { market: 'BTC', min_price: '1000' } // params as Record<string, string>
411
+ );
412
+ // { success, query_name, data, count, query_time_ms }
413
+
414
+ // Execute a specific version
415
+ const v2data = await db.executeQuery('active_markets', { market: 'ETH' }, 2);
416
+
417
+ // Delete a query
418
+ await db.deleteQuery('active_markets');
409
419
  ```
410
420
 
411
421
  ## Materialized Views
412
422
 
413
- Create pre-computed views for complex queries:
414
-
415
- ### Create View
423
+ ### Create via JSON Query
416
424
 
417
425
  ```typescript
418
- const view = await db.createView(
419
- 'active_users_summary',
420
- ['users', 'orders'], // Source collections
421
- {
422
- // Query definition
423
- filter: { status: 'active' },
424
- groupBy: 'region',
425
- aggregate: { totalOrders: { $count: 'orders' } },
426
- }
426
+ const db_manager = db.database();
427
+
428
+ await db_manager.createView(
429
+ 'top_sellers',
430
+ ['products', 'orders'],
431
+ { find: { status: { $eq: 'completed' } }, sort_by: ['-total'], limit: 100 }
427
432
  );
428
433
  ```
429
434
 
430
- ### List Views
435
+ ### Create via SQL
431
436
 
432
437
  ```typescript
433
- const views = await db.listViews();
434
- // [{ name: 'active_users_summary', source_collections: [...], created_at: '...' }]
438
+ await db_manager.createViewSql(
439
+ 'SELECT market, AVG(price) as avg_price FROM myapp::trades GROUP BY market',
440
+ 'live' // 'live' | 'lazy'
441
+ );
435
442
  ```
436
443
 
437
- ### Get View Details
444
+ ### Query & Manage Views
438
445
 
439
446
  ```typescript
440
- const view = await db.getView('active_users_summary');
441
- // { name: '...', source_collections: [...], query: {...}, created_at: '...' }
447
+ // Query view data
448
+ const result = await db_manager.queryView<TradeStats>('top_sellers', {
449
+ find: { avg_price: { $gte: 1000 } },
450
+ sort_by: ['-avg_price'],
451
+ limit: 20
452
+ });
453
+ // { success, view_name, data: T[], count, query_time_ms }
454
+
455
+ // Count records in a view
456
+ const count = await db_manager.countView('top_sellers', { market: 'BTC' });
457
+
458
+ await db_manager.listViews();
459
+ await db_manager.getView('top_sellers');
460
+ await db_manager.refreshView('top_sellers');
461
+ await db_manager.deleteView('top_sellers');
442
462
  ```
443
463
 
444
- ### Refresh View
464
+ ## API Collections
465
+
466
+ Collections backed by external APIs instead of on-chain data.
445
467
 
446
468
  ```typescript
447
- await db.refreshView('active_users_summary');
469
+ const db_manager = db.database();
470
+
471
+ await db_manager.createApiCollection({
472
+ name: 'live_prices',
473
+ description: 'Real-time price feed from external API',
474
+ api_config: {
475
+ base_url: 'https://api.example.com',
476
+ authentication: { type: 'bearer', token: 'xxx' },
477
+ query_mapping: { path_template: '/v1/prices/{market}' },
478
+ response_mapping: { records_path: 'data.prices' }
479
+ }
480
+ });
481
+
482
+ const collections = await db_manager.listApiCollections();
483
+ const collection = await db_manager.getApiCollection('live_prices');
484
+ await db_manager.deleteApiCollection('live_prices');
448
485
  ```
449
486
 
450
- ### Delete View
487
+ ## Index Management
488
+
489
+ Access the `DatabaseManager` via `db.database()` for direct index control.
451
490
 
452
491
  ```typescript
453
- await db.deleteView('active_users_summary');
492
+ const dbm = db.database();
493
+
494
+ // List all indexes
495
+ const indexes = await dbm.listIndexes();
496
+
497
+ // Create an index
498
+ await dbm.createIndex({
499
+ name: 'users_email_idx',
500
+ collection: 'users',
501
+ field_name: 'email',
502
+ index_type: 'btree'
503
+ });
504
+
505
+ // Create multiple indexes at once
506
+ await dbm.createIndexes('users', [
507
+ { name: 'email_idx', field_name: 'email', index_type: 'btree' },
508
+ { name: 'status_idx', field_name: 'status', index_type: 'hash' },
509
+ { name: 'bio_idx', field_name: 'bio', index_type: 'fulltext' },
510
+ ]);
511
+
512
+ // Create a price index (enables payment splits on write)
513
+ await dbm.createPriceIndex('orders', 'total_price', {
514
+ app_owner_percentage: 0.80,
515
+ platform_percentage: 0.20,
516
+ app_owner_address: 'celestia1abc...'
517
+ });
518
+
519
+ // Drop an index by ID
520
+ await dbm.dropIndex('users_email_idx');
454
521
  ```
455
522
 
456
- ## Real-World Example: Social App
523
+ ## Blob Storage
457
524
 
458
525
  ```typescript
459
- import { createClient, OnChainDBClient } from '@onchaindb/sdk';
526
+ // Upload
527
+ const upload = await db.uploadBlob({
528
+ collection: 'images',
529
+ blob: file,
530
+ metadata: { user_address: userAddress }
531
+ });
460
532
 
461
- class SocialService {
462
- private client: OnChainDBClient;
533
+ // Wait for on-chain confirmation
534
+ const task = await db.waitForTaskCompletion(upload.ticket_id);
535
+ console.log('Blob ID:', upload.blob_id);
463
536
 
464
- constructor(endpoint: string, appId: string, apiKey?: string) {
465
- this.client = createClient({ endpoint, appId, apiKey });
466
- }
537
+ // Retrieve
538
+ const blob = await db.retrieveBlob({
539
+ collection: 'images',
540
+ blob_id: upload.blob_id
541
+ });
542
+ ```
467
543
 
468
- async getTimeline(limit = 20, offset = 0) {
469
- const result = await this.client.queryBuilder()
470
- .collection('tweets')
471
- .whereField('reply_to_id').isNull()
472
- .joinOne('author_info', 'users')
473
- .onField('address').equals('$data.author')
474
- .selectFields(['display_name', 'avatar_url', 'verified'])
475
- .build()
476
- .joinMany('likes', 'likes')
477
- .onField('tweet_id').equals('$data.id')
478
- .selectFields(['user'])
479
- .build()
480
- .joinMany('replies', 'tweets')
481
- .onField('reply_to_id').equals('$data.id')
482
- .selectFields(['id'])
483
- .build()
484
- .selectAll()
485
- .offset(offset)
486
- .limit(limit)
487
- .execute();
488
-
489
- return result.records.map(tweet => ({
490
- ...tweet,
491
- like_count: tweet.likes?.length || 0,
492
- reply_count: tweet.replies?.length || 0
493
- }));
494
- }
544
+ ## Task Tracking
495
545
 
496
- async createTweet(content: string, author: string, paymentCallback: Function) {
497
- const tweet = {
498
- id: `tweet_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`,
499
- content,
500
- author,
501
- created_at: new Date().toISOString()
502
- };
546
+ ```typescript
547
+ const task = await db.getTaskStatus(ticketId);
548
+ // status: 'Pending' | 'PaymentBroadcast' | 'PaymentConfirming' |
549
+ // 'PaymentConfirmed' | 'StoringData' | 'Completed' | { Failed: { error } }
503
550
 
504
- await this.client.store(
505
- { collection: 'tweets', data: [tweet] },
506
- paymentCallback
507
- );
551
+ const completed = await db.waitForTaskCompletion(ticketId, 2000, 600000);
552
+ // Polls every 2s, times out after 600s
553
+ ```
508
554
 
509
- return tweet;
510
- }
555
+ ## Pricing Quotes
511
556
 
512
- async getUserProfile(address: string) {
513
- const result = await this.client.queryBuilder()
514
- .collection('users')
515
- .whereField('address').equals(address)
516
- .joinMany('tweets', 'tweets')
517
- .onField('author').equals('$data.address')
518
- .selectAll()
519
- .build()
520
- .joinMany('followers', 'follows')
521
- .onField('following').equals('$data.address')
522
- .selectFields(['follower'])
523
- .build()
524
- .joinMany('following', 'follows')
525
- .onField('follower').equals('$data.address')
526
- .selectFields(['following'])
527
- .build()
528
- .selectAll()
529
- .limit(1)
530
- .execute();
531
-
532
- if (!result.records.length) return null;
533
-
534
- const user = result.records[0];
535
- return {
536
- ...user,
537
- follower_count: user.followers?.length || 0,
538
- following_count: user.following?.length || 0
539
- };
540
- }
541
- }
557
+ ```typescript
558
+ const quote = await db.getPricingQuote({
559
+ app_id: 'my_app',
560
+ operation_type: 'write',
561
+ size_kb: 10,
562
+ collection: 'orders',
563
+ data: { amount: 150 } // for price index calculation
564
+ });
565
+ // { total_cost, total_cost_utia, indexing_costs, ... }
542
566
  ```
543
567
 
544
568
  ## Error Handling
545
569
 
546
570
  ```typescript
547
- import { OnChainDBError, ValidationError } from '@onchaindb/sdk';
571
+ import { OnChainDBError, ValidationError, PaymentRequiredError } from '@onchaindb/sdk';
548
572
 
549
573
  try {
550
- await db.store({ collection: 'test', data: [{ test: 'data' }] });
574
+ await db.store({ collection: 'test', data: [{ value: 1 }] });
551
575
  } catch (error) {
552
- if (error instanceof ValidationError) {
553
- console.log('Validation failed:', error.message);
554
- } else if (error instanceof OnChainDBError) {
555
- console.log('Error:', error.code, error.message);
556
- }
576
+ if (error instanceof ValidationError) console.log('Validation:', error.message);
577
+ if (error instanceof PaymentRequiredError) console.log('Quote:', error.quote);
578
+ if (error instanceof OnChainDBError) console.log(error.code, error.message);
557
579
  }
558
580
  ```
559
581
 
@@ -563,37 +585,82 @@ try {
563
585
 
564
586
  | Method | Description |
565
587
  |--------|-------------|
566
- | `store(request, paymentCallback?, wait?)` | Store data |
567
- | `queryBuilder()` | Create query builder |
568
- | `uploadBlob(request)` | Upload binary file |
569
- | `retrieveBlob(request)` | Download binary file |
570
- | `getTaskStatus(ticketId)` | Get task status |
571
- | `waitForTaskCompletion(ticketId)` | Poll until complete |
572
- | `getPricingQuote(request)` | Get pricing quote |
588
+ | `store(request, paymentCallback?)` | Store data on-chain |
589
+ | `queryBuilder()` | Fluent query builder |
573
590
  | `createCollection(schema)` | Create collection with indexes |
574
- | `syncCollection(schema)` | Sync indexes to match schema |
591
+ | `syncCollection(schema)` | Sync indexes to schema (supports sharding) |
592
+ | `setupSharding(collection, sharding)` | Configure sharding on an existing collection |
593
+ | `deleteCollection(collection)` | Delete a collection |
594
+ | `updateCollection(collection, updates)` | Update collection properties |
595
+ | `getCollectionInfo(collectionId)` | Get collection details by ID |
596
+ | `getRetention(collection)` | Get retention config |
597
+ | `setRetention(collection, request)` | Set retention period |
598
+ | `getRetentionCost()` | Get retention cost estimate for all collections |
599
+ | `sql(query, options?)` | Execute SQL query |
600
+ | `sqlInsert(sql)` | Execute SQL insert |
601
+ | `listQueries()` | List predefined queries |
602
+ | `createQuery(request)` | Create a predefined query |
603
+ | `getQuery(name)` | Get query definition |
604
+ | `deleteQuery(name)` | Delete a predefined query |
605
+ | `executeQuery(name, params?, version?)` | Execute a predefined query |
606
+ | `createRelation(request)` | Create a collection relation |
607
+ | `createIndex(request)` | Create a single index |
608
+ | `getPricingQuote(request)` | Estimate storage cost |
609
+ | `uploadBlob(request, paymentCallback?)` | Upload binary file |
610
+ | `retrieveBlob(request)` | Download binary file |
611
+ | `getTaskStatus(ticketId)` | Get async task status |
612
+ | `waitForTaskCompletion(ticketId)` | Poll until task completes |
613
+ | `health()` | Server health check |
614
+ | `database(appId?)` | Access `DatabaseManager` |
615
+
616
+ ### DatabaseManager (`db.database()`)
617
+
618
+ | Method | Description |
619
+ |--------|-------------|
620
+ | `listIndexes()` | List all indexes for the app |
621
+ | `createIndex(definition)` | Create an index |
622
+ | `createIndexes(collection, indexes)` | Bulk create indexes |
623
+ | `createPriceIndex(collection, field, config?)` | Create payment-split price index |
624
+ | `dropIndex(indexId)` | Delete an index by ID |
575
625
  | `createView(name, sources, query)` | Create materialized view |
626
+ | `createViewSql(sql, refreshMode?)` | Create view from SQL |
627
+ | `queryView(viewName, query?)` | Query view data |
628
+ | `countView(viewName, filter?)` | Count view records |
576
629
  | `listViews()` | List all views |
577
- | `getView(name)` | Get view details |
630
+ | `getView(name)` | Get view definition |
631
+ | `refreshView(name)` | Rebuild view |
578
632
  | `deleteView(name)` | Delete a view |
579
- | `refreshView(name)` | Refresh view data |
580
- | `health()` | Health check |
633
+ | `createApiCollection(request)` | Create API-backed collection |
634
+ | `listApiCollections()` | List API collections |
635
+ | `getApiCollection(name)` | Get API collection details |
636
+ | `deleteApiCollection(name)` | Delete API collection |
581
637
 
582
638
  ### QueryBuilder
583
639
 
584
640
  | Method | Description |
585
641
  |--------|-------------|
586
- | `collection(name)` | Set collection |
587
- | `whereField(name)` | Start field condition |
588
- | `find(builderFn)` | Complex conditions |
642
+ | `collection(name)` | Set target collection |
643
+ | `whereField(name)` | Start a field condition |
644
+ | `find(builderFn)` | Set complex logical conditions |
589
645
  | `selectFields(fields)` | Select specific fields |
590
646
  | `selectAll()` | Select all fields |
591
- | `joinOne(alias, model)` | One-to-one JOIN |
592
- | `joinMany(alias, model)` | One-to-many JOIN |
593
- | `limit(n)` | Limit results |
647
+ | `joinOne(alias, model)` | One-to-one server JOIN |
648
+ | `joinMany(alias, model)` | One-to-many server JOIN |
649
+ | `orderBy(field, dir?)` | Sort results |
650
+ | `limit(n)` | Limit result count |
594
651
  | `offset(n)` | Skip results |
595
- | `execute()` | Execute query |
652
+ | `execute()` | Execute and return records |
653
+ | `executeUnique()` | Return latest single record |
654
+ | `count()` | Count matching records |
655
+ | `sumBy(field)` | Sum numeric field |
656
+ | `avgBy(field)` | Average numeric field |
657
+ | `maxBy(field)` | Maximum field value |
658
+ | `minBy(field)` | Minimum field value |
659
+ | `distinctBy(field)` | Distinct field values |
660
+ | `groupBy(field)` | Start grouped aggregation |
661
+ | `buildRawQuery()` | Inspect raw query object |
662
+ | `clone()` | Clone builder |
596
663
 
597
664
  ## License
598
665
 
599
- MIT License
666
+ MIT