@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.
- package/.claude/settings.local.json +10 -2
- package/README.md +422 -355
- package/dist/batch.d.ts +1 -10
- package/dist/batch.d.ts.map +1 -1
- package/dist/batch.js +4 -26
- package/dist/batch.js.map +1 -1
- package/dist/client.d.ts +29 -43
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +199 -323
- package/dist/client.js.map +1 -1
- package/dist/database.d.ts +14 -131
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +35 -131
- package/dist/database.js.map +1 -1
- package/dist/index.d.ts +6 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -15
- package/dist/index.js.map +1 -1
- package/dist/query-sdk/ConditionBuilder.d.ts +3 -11
- package/dist/query-sdk/ConditionBuilder.d.ts.map +1 -1
- package/dist/query-sdk/ConditionBuilder.js +10 -48
- package/dist/query-sdk/ConditionBuilder.js.map +1 -1
- package/dist/query-sdk/NestedBuilders.d.ts +33 -30
- package/dist/query-sdk/NestedBuilders.d.ts.map +1 -1
- package/dist/query-sdk/NestedBuilders.js +46 -43
- package/dist/query-sdk/NestedBuilders.js.map +1 -1
- package/dist/query-sdk/QueryBuilder.d.ts +4 -2
- package/dist/query-sdk/QueryBuilder.d.ts.map +1 -1
- package/dist/query-sdk/QueryBuilder.js +47 -169
- package/dist/query-sdk/QueryBuilder.js.map +1 -1
- package/dist/query-sdk/QueryResult.d.ts +0 -38
- package/dist/query-sdk/QueryResult.d.ts.map +1 -1
- package/dist/query-sdk/QueryResult.js +1 -227
- package/dist/query-sdk/QueryResult.js.map +1 -1
- package/dist/query-sdk/index.d.ts +1 -1
- package/dist/query-sdk/index.d.ts.map +1 -1
- package/dist/query-sdk/index.js.map +1 -1
- package/dist/query-sdk/operators.d.ts +32 -28
- package/dist/query-sdk/operators.d.ts.map +1 -1
- package/dist/query-sdk/operators.js +45 -155
- package/dist/query-sdk/operators.js.map +1 -1
- package/dist/types.d.ts +153 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/jest.config.js +4 -0
- package/package.json +1 -1
- package/skills.md +0 -1
- package/src/client.ts +243 -745
- package/src/database.ts +70 -493
- package/src/index.ts +40 -193
- package/src/query-sdk/ConditionBuilder.ts +37 -89
- package/src/query-sdk/NestedBuilders.ts +90 -92
- package/src/query-sdk/QueryBuilder.ts +59 -218
- package/src/query-sdk/QueryResult.ts +4 -330
- package/src/query-sdk/README.md +214 -583
- package/src/query-sdk/index.ts +1 -1
- package/src/query-sdk/operators.ts +91 -200
- package/src/query-sdk/tests/FieldConditionBuilder.test.ts +70 -71
- package/src/query-sdk/tests/LogicalOperator.test.ts +43 -82
- package/src/query-sdk/tests/NestedBuilders.test.ts +229 -309
- package/src/query-sdk/tests/QueryBuilder.test.ts +5 -5
- package/src/query-sdk/tests/QueryResult.test.ts +41 -435
- package/src/query-sdk/tests/comprehensive.test.ts +4 -185
- package/src/tests/client-requests.test.ts +280 -0
- package/src/tests/client-validation.test.ts +80 -0
- package/src/types.ts +229 -8
- package/src/batch.ts +0 -257
- package/src/query-sdk/dist/ConditionBuilder.d.ts +0 -22
- package/src/query-sdk/dist/ConditionBuilder.js +0 -90
- package/src/query-sdk/dist/FieldConditionBuilder.d.ts +0 -1
- package/src/query-sdk/dist/FieldConditionBuilder.js +0 -6
- package/src/query-sdk/dist/NestedBuilders.d.ts +0 -43
- package/src/query-sdk/dist/NestedBuilders.js +0 -144
- package/src/query-sdk/dist/OnChainDB.d.ts +0 -19
- package/src/query-sdk/dist/OnChainDB.js +0 -123
- package/src/query-sdk/dist/QueryBuilder.d.ts +0 -70
- package/src/query-sdk/dist/QueryBuilder.js +0 -295
- package/src/query-sdk/dist/QueryResult.d.ts +0 -52
- package/src/query-sdk/dist/QueryResult.js +0 -293
- package/src/query-sdk/dist/SelectionBuilder.d.ts +0 -20
- package/src/query-sdk/dist/SelectionBuilder.js +0 -80
- package/src/query-sdk/dist/adapters/HttpClientAdapter.d.ts +0 -27
- package/src/query-sdk/dist/adapters/HttpClientAdapter.js +0 -170
- package/src/query-sdk/dist/index.d.ts +0 -36
- package/src/query-sdk/dist/index.js +0 -27
- package/src/query-sdk/dist/operators.d.ts +0 -56
- package/src/query-sdk/dist/operators.js +0 -289
- package/src/query-sdk/dist/tests/setup.d.ts +0 -15
- package/src/query-sdk/dist/tests/setup.js +0 -46
- package/src/query-sdk/jest.config.js +0 -25
- package/src/query-sdk/package.json +0 -46
- package/src/query-sdk/tests/aggregations.test.ts +0 -653
- package/src/query-sdk/tests/integration.test.ts +0 -608
- package/src/query-sdk/tests/operators.test.ts +0 -327
- package/src/query-sdk/tests/unit.test.ts +0 -794
- package/src/query-sdk/tsconfig.json +0 -26
- package/src/query-sdk/yarn.lock +0 -3092
package/src/client.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import axios, {AxiosError, AxiosInstance} from 'axios';
|
|
2
2
|
import {EventEmitter} from 'eventemitter3';
|
|
3
3
|
import {
|
|
4
|
-
BlobMetadata,
|
|
5
4
|
CreateCollectionResult,
|
|
6
5
|
IndexRequest,
|
|
7
6
|
IndexResponse,
|
|
@@ -13,12 +12,25 @@ import {
|
|
|
13
12
|
RelationResponse,
|
|
14
13
|
RetrieveBlobRequest,
|
|
15
14
|
SimpleCollectionSchema,
|
|
15
|
+
SimpleCollectionSchemaWithSharding,
|
|
16
16
|
SimpleFieldDefinition,
|
|
17
|
+
ShardingStrategy,
|
|
18
|
+
UpdateCollectionRequest,
|
|
19
|
+
SetRetentionRequest,
|
|
20
|
+
RetentionConfig,
|
|
21
|
+
RetentionCostResponse,
|
|
22
|
+
SqlQueryRequest,
|
|
23
|
+
SqlQueryResponse,
|
|
24
|
+
SqlInsertRequest,
|
|
25
|
+
CreateQueryRequest,
|
|
26
|
+
CreateQueryResponse,
|
|
27
|
+
QueryDefinition,
|
|
28
|
+
QueryDataResponse,
|
|
29
|
+
CollectionResponse,
|
|
17
30
|
StoreRequest,
|
|
18
31
|
StoreResponse,
|
|
19
32
|
SyncCollectionResult,
|
|
20
33
|
TaskInfo,
|
|
21
|
-
TransactionError,
|
|
22
34
|
TransactionEvents,
|
|
23
35
|
UploadBlobRequest,
|
|
24
36
|
UploadBlobResponse,
|
|
@@ -28,7 +40,7 @@ import {
|
|
|
28
40
|
// Import query builder components
|
|
29
41
|
// Import database management
|
|
30
42
|
import {createDatabaseManager, DatabaseManager} from './database';
|
|
31
|
-
import {
|
|
43
|
+
import {QueryBuilder} from "./query-sdk";
|
|
32
44
|
|
|
33
45
|
// Import x402 utilities
|
|
34
46
|
import {
|
|
@@ -53,12 +65,18 @@ import {
|
|
|
53
65
|
*
|
|
54
66
|
* @example
|
|
55
67
|
* ```typescript
|
|
56
|
-
* // Initialize with app key (for writes)
|
|
68
|
+
* // Initialize with app key (for writes)
|
|
57
69
|
* const db = new OnChainDBClient({
|
|
58
70
|
* endpoint: 'http://localhost:9092',
|
|
59
71
|
* appId: 'app_abc123',
|
|
60
|
-
* appKey: 'app_xxx...', // Required for write operations
|
|
61
|
-
*
|
|
72
|
+
* appKey: 'app_xxx...', // Required for write operations (X-App-Key header)
|
|
73
|
+
* });
|
|
74
|
+
*
|
|
75
|
+
* // Agent Key: autonomous agent that pays other apps inline via USDC (EIP-3009)
|
|
76
|
+
* const agent = new OnChainDBClient({
|
|
77
|
+
* endpoint: 'http://localhost:9092',
|
|
78
|
+
* appId: 'app_abc123',
|
|
79
|
+
* agentKey: 'agent_zzz...', // X-Agent-Key header — key must have Pay permission
|
|
62
80
|
* });
|
|
63
81
|
*
|
|
64
82
|
* // Store data (requires appKey)
|
|
@@ -67,7 +85,7 @@ import {
|
|
|
67
85
|
* collection: 'messages'
|
|
68
86
|
* });
|
|
69
87
|
*
|
|
70
|
-
* // Query data
|
|
88
|
+
* // Query data
|
|
71
89
|
* const messages = await db.query({ collection: 'messages' });
|
|
72
90
|
*
|
|
73
91
|
* // Backwards compatible: legacy apiKey maps to appKey
|
|
@@ -88,7 +106,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
88
106
|
deletedAt: {type: 'date', index: true}
|
|
89
107
|
};
|
|
90
108
|
private http: AxiosInstance;
|
|
91
|
-
private config: Required<Omit<OnChainDBConfig, 'appId'>> & { appId?: string; appKey?: string; userKey?: string };
|
|
109
|
+
private config: Required<Omit<OnChainDBConfig, 'appId'>> & { appId?: string; appKey?: string; userKey?: string; agentKey?: string };
|
|
92
110
|
private _database?: DatabaseManager;
|
|
93
111
|
|
|
94
112
|
constructor(config: OnChainDBConfig) {
|
|
@@ -97,19 +115,21 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
97
115
|
// Support legacy apiKey for backwards compatibility (maps to appKey)
|
|
98
116
|
const appKey = config.appKey || '';
|
|
99
117
|
const userKey = config.userKey || '';
|
|
118
|
+
const agentKey = config.agentKey || '';
|
|
100
119
|
|
|
101
120
|
this.config = {
|
|
102
121
|
endpoint: config.endpoint,
|
|
103
122
|
apiKey: config.apiKey || "", // Keep for backwards compat, but maps to appKey
|
|
104
123
|
appKey: appKey,
|
|
105
124
|
userKey: userKey,
|
|
125
|
+
agentKey: agentKey,
|
|
106
126
|
appId: config.appId || undefined,
|
|
107
127
|
timeout: config.timeout || 30000,
|
|
108
128
|
retryCount: config.retryCount || 3,
|
|
109
129
|
retryDelay: config.retryDelay || 1000
|
|
110
130
|
};
|
|
111
131
|
|
|
112
|
-
// Build headers with
|
|
132
|
+
// Build headers with provided keys
|
|
113
133
|
const headers: Record<string, string> = {
|
|
114
134
|
'Content-Type': 'application/json'
|
|
115
135
|
};
|
|
@@ -122,11 +142,15 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
122
142
|
headers['X-Api-Key'] = config.apiKey;
|
|
123
143
|
}
|
|
124
144
|
|
|
125
|
-
|
|
126
145
|
if (userKey) {
|
|
127
146
|
headers['X-User-Key'] = userKey;
|
|
128
147
|
}
|
|
129
148
|
|
|
149
|
+
// Agent Keys use X-Agent-Key (not X-API-Key / X-App-Key)
|
|
150
|
+
if (agentKey) {
|
|
151
|
+
headers['X-Agent-Key'] = agentKey;
|
|
152
|
+
}
|
|
153
|
+
|
|
130
154
|
this.http = axios.create({
|
|
131
155
|
baseURL: this.config.endpoint,
|
|
132
156
|
timeout: this.config.timeout,
|
|
@@ -162,12 +186,15 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
162
186
|
* });
|
|
163
187
|
* ```
|
|
164
188
|
*/
|
|
165
|
-
database(appId
|
|
166
|
-
|
|
189
|
+
database(appId?: string): DatabaseManager {
|
|
190
|
+
const resolvedAppId = appId || this.config.appId;
|
|
191
|
+
if (!resolvedAppId) {
|
|
192
|
+
throw new ValidationError('appId must be provided either in config or as an argument to database()');
|
|
193
|
+
}
|
|
194
|
+
if (!this._database || this._database['appId'] !== resolvedAppId) {
|
|
167
195
|
this._database = createDatabaseManager(
|
|
168
196
|
this.http,
|
|
169
|
-
|
|
170
|
-
appId,
|
|
197
|
+
resolvedAppId,
|
|
171
198
|
this.config.apiKey
|
|
172
199
|
);
|
|
173
200
|
}
|
|
@@ -187,7 +214,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
187
214
|
* @param waitForConfirmation - Whether to wait for blockchain confirmation
|
|
188
215
|
* @returns Promise resolving to the operation result
|
|
189
216
|
*/
|
|
190
|
-
async handleX402(
|
|
217
|
+
private async handleX402(
|
|
191
218
|
response: { data: any },
|
|
192
219
|
paymentCallback: ((quote: X402Quote) => Promise<X402PaymentCallbackResult>) | undefined,
|
|
193
220
|
finalRequest: any,
|
|
@@ -295,29 +322,11 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
295
322
|
if (serverResult.ticket_id) {
|
|
296
323
|
if (waitForConfirmation) {
|
|
297
324
|
const taskInfo = await this.waitForTaskCompletion(serverResult.ticket_id);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
const firstResult = taskInfo.result.results[0];
|
|
301
|
-
return {
|
|
302
|
-
id: firstResult.id,
|
|
303
|
-
block_height: firstResult.celestia_height || 0,
|
|
304
|
-
transaction_hash: firstResult.blob_id || '',
|
|
305
|
-
celestia_height: firstResult.celestia_height || 0,
|
|
306
|
-
namespace: firstResult.namespace || '',
|
|
307
|
-
confirmed: firstResult.celestia_height > 0
|
|
308
|
-
};
|
|
325
|
+
if (taskInfo.result?.results?.length > 0) {
|
|
326
|
+
return this.buildStoreResult(taskInfo.result.results[0]);
|
|
309
327
|
}
|
|
310
328
|
} else {
|
|
311
|
-
|
|
312
|
-
return {
|
|
313
|
-
id: serverResult.ticket_id,
|
|
314
|
-
block_height: 0,
|
|
315
|
-
transaction_hash: '',
|
|
316
|
-
celestia_height: 0,
|
|
317
|
-
namespace: '',
|
|
318
|
-
confirmed: false,
|
|
319
|
-
ticket_id: serverResult.ticket_id
|
|
320
|
-
} as StoreResponse;
|
|
329
|
+
return { id: serverResult.ticket_id, block_height: 0, transaction_hash: '', celestia_height: 0, namespace: '', confirmed: false, ticket_id: serverResult.ticket_id } as StoreResponse;
|
|
321
330
|
}
|
|
322
331
|
}
|
|
323
332
|
|
|
@@ -368,18 +377,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
368
377
|
delete resolvedRequest.collection;
|
|
369
378
|
|
|
370
379
|
|
|
371
|
-
// Step 1: Try to store without payment (may get 402)
|
|
372
380
|
const response = await this.http.post('/store', resolvedRequest);
|
|
373
|
-
|
|
374
|
-
// Step 2: Handle 402 Payment Required (x402)
|
|
375
|
-
if (response.status === 402 && response.data) {
|
|
376
|
-
const v = await this.handleX402(response, paymentCallback, resolvedRequest, waitForConfirmation);
|
|
377
|
-
|
|
378
|
-
if (v) {
|
|
379
|
-
return v;
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
381
|
const serverResult = response.data;
|
|
384
382
|
|
|
385
383
|
// Check if we got an async response with ticket_id (new flow)
|
|
@@ -396,20 +394,8 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
396
394
|
const taskInfo = await this.waitForTaskCompletion(serverResult.ticket_id);
|
|
397
395
|
|
|
398
396
|
// Extract the actual storage result from the completed task
|
|
399
|
-
if (taskInfo.result
|
|
400
|
-
const
|
|
401
|
-
|
|
402
|
-
// Transform to SDK format
|
|
403
|
-
const result: StoreResponse = {
|
|
404
|
-
id: firstResult.id,
|
|
405
|
-
block_height: firstResult.celestia_height || 0,
|
|
406
|
-
transaction_hash: firstResult.blob_id || '',
|
|
407
|
-
celestia_height: firstResult.celestia_height || 0,
|
|
408
|
-
namespace: firstResult.namespace || '',
|
|
409
|
-
confirmed: firstResult.celestia_height > 0
|
|
410
|
-
};
|
|
411
|
-
|
|
412
|
-
// Emit completion event
|
|
397
|
+
if (taskInfo.result?.results?.length > 0) {
|
|
398
|
+
const result = this.buildStoreResult(taskInfo.result.results[0]);
|
|
413
399
|
this.emit('transaction:confirmed', {
|
|
414
400
|
id: result.id,
|
|
415
401
|
status: 'confirmed',
|
|
@@ -417,40 +403,22 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
417
403
|
transaction_hash: result.transaction_hash,
|
|
418
404
|
celestia_height: result.celestia_height
|
|
419
405
|
});
|
|
420
|
-
|
|
421
406
|
return result;
|
|
422
407
|
} else {
|
|
423
408
|
throw new OnChainDBError('Task completed but no storage results found', 'STORE_ERROR');
|
|
424
409
|
}
|
|
425
410
|
} else {
|
|
426
|
-
|
|
427
|
-
return {
|
|
428
|
-
id: serverResult.ticket_id,
|
|
429
|
-
block_height: 0,
|
|
430
|
-
transaction_hash: '',
|
|
431
|
-
celestia_height: 0,
|
|
432
|
-
namespace: '',
|
|
433
|
-
confirmed: false,
|
|
434
|
-
ticket_id: serverResult.ticket_id
|
|
435
|
-
} as StoreResponse;
|
|
411
|
+
return { id: serverResult.ticket_id, block_height: 0, transaction_hash: '', celestia_height: 0, namespace: '', confirmed: false, ticket_id: serverResult.ticket_id } as StoreResponse;
|
|
436
412
|
}
|
|
437
413
|
}
|
|
438
414
|
|
|
439
415
|
// Legacy response format (if server returns old format)
|
|
440
|
-
const firstResult = serverResult.results
|
|
416
|
+
const firstResult = serverResult.results?.[0];
|
|
441
417
|
if (!firstResult) {
|
|
442
418
|
throw new OnChainDBError('No results returned from server', 'STORE_ERROR');
|
|
443
419
|
}
|
|
444
420
|
|
|
445
|
-
|
|
446
|
-
const result: StoreResponse = {
|
|
447
|
-
id: firstResult.id,
|
|
448
|
-
block_height: firstResult.celestia_height || 0,
|
|
449
|
-
transaction_hash: firstResult.blob_id || '',
|
|
450
|
-
celestia_height: firstResult.celestia_height || 0,
|
|
451
|
-
namespace: firstResult.namespace || '',
|
|
452
|
-
confirmed: firstResult.celestia_height > 0
|
|
453
|
-
};
|
|
421
|
+
const result = this.buildStoreResult(firstResult);
|
|
454
422
|
|
|
455
423
|
// Emit appropriate event based on height
|
|
456
424
|
if (result.block_height === 0) {
|
|
@@ -470,10 +438,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
470
438
|
});
|
|
471
439
|
}
|
|
472
440
|
|
|
473
|
-
if (waitForConfirmation && result.block_height === 0) {
|
|
474
|
-
return await this.waitForConfirmation(result.transaction_hash);
|
|
475
|
-
}
|
|
476
|
-
|
|
477
441
|
return result;
|
|
478
442
|
} catch (error: any) {
|
|
479
443
|
if ((error as AxiosError)?.response) {
|
|
@@ -489,6 +453,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
489
453
|
throw new OnChainDBError(JSON.stringify(err.response?.data), 'STORE_ERROR');
|
|
490
454
|
}
|
|
491
455
|
|
|
456
|
+
console.error(error)
|
|
492
457
|
const dbError = error instanceof OnChainDBError ? error :
|
|
493
458
|
new OnChainDBError(error.message, 'STORE_ERROR');
|
|
494
459
|
this.emit('error', dbError);
|
|
@@ -496,98 +461,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
496
461
|
}
|
|
497
462
|
}
|
|
498
463
|
|
|
499
|
-
/**
|
|
500
|
-
* Store data and return a promise that resolves when transaction is confirmed
|
|
501
|
-
*
|
|
502
|
-
* @param request - Store request
|
|
503
|
-
* @param paymentOptions - Payment configuration for broker fees
|
|
504
|
-
* @returns Promise resolving when transaction is confirmed on blockchain
|
|
505
|
-
*/
|
|
506
|
-
async storeAndConfirm(
|
|
507
|
-
request: StoreRequest
|
|
508
|
-
): Promise<StoreResponse> {
|
|
509
|
-
return this.store(request, undefined, true);
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
/**
|
|
513
|
-
* Wait for transaction confirmation
|
|
514
|
-
*
|
|
515
|
-
* @param transactionHash - Transaction hash to monitor
|
|
516
|
-
* @param maxWaitTime - Maximum wait time in milliseconds (default: 5 minutes)
|
|
517
|
-
* @returns Promise resolving to confirmed transaction
|
|
518
|
-
*/
|
|
519
|
-
async waitForConfirmation(transactionHash: string, maxWaitTime: number = 300000): Promise<StoreResponse> {
|
|
520
|
-
const startTime = Date.now();
|
|
521
|
-
const pollInterval = 3000; // Poll every 3 seconds (same as server)
|
|
522
|
-
|
|
523
|
-
console.log(`🔄 Waiting for transaction ${transactionHash} confirmation...`);
|
|
524
|
-
|
|
525
|
-
while (Date.now() - startTime < maxWaitTime) {
|
|
526
|
-
try {
|
|
527
|
-
// Query Celestia RPC directly to check transaction status
|
|
528
|
-
const rpcUrl = 'https://celestia-mocha-rpc.publicnode.com:443'; // Default testnet RPC
|
|
529
|
-
const txUrl = `${rpcUrl}/tx?hash=0x${transactionHash}`;
|
|
530
|
-
|
|
531
|
-
console.log(`🔍 Checking transaction status: attempt ${Math.floor((Date.now() - startTime) / pollInterval) + 1}`);
|
|
532
|
-
|
|
533
|
-
const response = await axios.get(txUrl);
|
|
534
|
-
|
|
535
|
-
if (response.data?.result && response.data.result !== null) {
|
|
536
|
-
const txResult = response.data.result;
|
|
537
|
-
const height = parseInt(txResult.height);
|
|
538
|
-
|
|
539
|
-
if (height > 0) {
|
|
540
|
-
console.log(`✅ Transaction ${transactionHash} confirmed at height ${height}`);
|
|
541
|
-
|
|
542
|
-
const confirmedTx: StoreResponse = {
|
|
543
|
-
id: transactionHash, // Use transaction hash as ID for confirmation
|
|
544
|
-
namespace: '', // Will be filled by actual usage
|
|
545
|
-
block_height: height,
|
|
546
|
-
transaction_hash: transactionHash,
|
|
547
|
-
celestia_height: height,
|
|
548
|
-
confirmed: true
|
|
549
|
-
};
|
|
550
|
-
|
|
551
|
-
this.emit('transaction:confirmed', {
|
|
552
|
-
id: transactionHash,
|
|
553
|
-
status: 'confirmed',
|
|
554
|
-
block_height: height,
|
|
555
|
-
transaction_hash: transactionHash
|
|
556
|
-
});
|
|
557
|
-
return confirmedTx;
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
|
|
561
|
-
// Still pending, wait and retry
|
|
562
|
-
console.log(`⏳ Transaction still pending, waiting ${pollInterval}ms...`);
|
|
563
|
-
this.emit('transaction:pending', {
|
|
564
|
-
id: transactionHash,
|
|
565
|
-
status: 'pending',
|
|
566
|
-
block_height: 0,
|
|
567
|
-
transaction_hash: transactionHash
|
|
568
|
-
});
|
|
569
|
-
await this.sleep(pollInterval);
|
|
570
|
-
|
|
571
|
-
} catch (error) {
|
|
572
|
-
// For 404 or other errors, the transaction might not be confirmed yet
|
|
573
|
-
if (Date.now() - startTime >= maxWaitTime) {
|
|
574
|
-
throw new TransactionError(
|
|
575
|
-
`Transaction confirmation timeout after ${maxWaitTime}ms`,
|
|
576
|
-
transactionHash
|
|
577
|
-
);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Wait and retry for temporary errors
|
|
581
|
-
console.log(`⚠️ Error checking transaction (will retry): ${error}`);
|
|
582
|
-
await this.sleep(pollInterval);
|
|
583
|
-
}
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
throw new TransactionError(
|
|
587
|
-
`Transaction confirmation timeout after ${maxWaitTime}ms`,
|
|
588
|
-
transactionHash
|
|
589
|
-
);
|
|
590
|
-
}
|
|
591
464
|
|
|
592
465
|
/**
|
|
593
466
|
* Create an index on a collection field
|
|
@@ -751,7 +624,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
751
624
|
* console.log('Removed:', result.removed); // [{ field: 'username', type: 'string' }]
|
|
752
625
|
* ```
|
|
753
626
|
*/
|
|
754
|
-
async syncCollection(schema:
|
|
627
|
+
async syncCollection(schema: SimpleCollectionSchemaWithSharding): Promise<SyncCollectionResult> {
|
|
755
628
|
const appId = this.config.appId;
|
|
756
629
|
if (!appId) {
|
|
757
630
|
throw new ValidationError('appId must be configured to sync collections');
|
|
@@ -766,22 +639,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
766
639
|
errors: []
|
|
767
640
|
};
|
|
768
641
|
|
|
769
|
-
// Get existing indexes for this collection
|
|
770
|
-
let existingIndexes: Array<{ field_name: string; index_type: string; name: string }> = [];
|
|
771
|
-
try {
|
|
772
|
-
const response = await this.http.get(`/api/apps/${appId}/collections/${schema.name}/indexes`);
|
|
773
|
-
existingIndexes = response.data?.indexes || response.data || [];
|
|
774
|
-
} catch (error) {
|
|
775
|
-
// Collection might not exist yet, that's okay
|
|
776
|
-
existingIndexes = [];
|
|
777
|
-
}
|
|
778
|
-
|
|
779
|
-
// Build map of existing indexes by field name
|
|
780
|
-
const existingByField = new Map<string, { type: string; name: string }>();
|
|
781
|
-
for (const idx of existingIndexes) {
|
|
782
|
-
existingByField.set(idx.field_name, {type: idx.index_type, name: idx.name});
|
|
783
|
-
}
|
|
784
|
-
|
|
785
642
|
// Merge base fields if enabled (default: true)
|
|
786
643
|
const allFields: Record<string, SimpleFieldDefinition> = {};
|
|
787
644
|
if (schema.useBaseFields !== false) {
|
|
@@ -797,9 +654,9 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
797
654
|
}
|
|
798
655
|
}
|
|
799
656
|
|
|
800
|
-
//
|
|
657
|
+
// Create all desired indexes (broker handles upsert)
|
|
801
658
|
for (const fieldName of desiredIndexedFields) {
|
|
802
|
-
|
|
659
|
+
{
|
|
803
660
|
const fieldDef = allFields[fieldName];
|
|
804
661
|
const indexType = fieldDef.indexType || this.getDefaultIndexType(fieldDef.type);
|
|
805
662
|
|
|
@@ -839,36 +696,179 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
839
696
|
}
|
|
840
697
|
}
|
|
841
698
|
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
field: fieldName,
|
|
851
|
-
type: existing.type
|
|
852
|
-
});
|
|
853
|
-
} catch (error) {
|
|
854
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
855
|
-
result.errors!.push(`Failed to remove index on ${fieldName}: ${errorMsg}`);
|
|
856
|
-
result.success = false;
|
|
857
|
-
}
|
|
699
|
+
if (schema.sharding) {
|
|
700
|
+
try {
|
|
701
|
+
await this.setupSharding(schema.name, schema.sharding);
|
|
702
|
+
result.sharding_configured = true;
|
|
703
|
+
} catch (error) {
|
|
704
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
705
|
+
result.errors!.push(`Failed to configure sharding: ${errorMsg}`);
|
|
706
|
+
result.success = false;
|
|
858
707
|
}
|
|
859
708
|
}
|
|
860
709
|
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
}
|
|
710
|
+
return result;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
async setupSharding(collection: string, sharding: ShardingStrategy): Promise<void> {
|
|
714
|
+
const appId = this.config.appId;
|
|
715
|
+
if (!appId) {
|
|
716
|
+
throw new ValidationError('appId must be configured to setup sharding');
|
|
869
717
|
}
|
|
718
|
+
try {
|
|
719
|
+
await this.http.patch(
|
|
720
|
+
`/api/apps/${appId}/collections/${collection}/sharding`,
|
|
721
|
+
sharding
|
|
722
|
+
);
|
|
723
|
+
} catch (error) {
|
|
724
|
+
throw error instanceof OnChainDBError ? error :
|
|
725
|
+
new OnChainDBError('Failed to setup sharding', 'SHARDING_ERROR');
|
|
726
|
+
}
|
|
727
|
+
}
|
|
870
728
|
|
|
871
|
-
|
|
729
|
+
async deleteCollection(collection: string): Promise<{ success: boolean }> {
|
|
730
|
+
const appId = this.config.appId;
|
|
731
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
732
|
+
try {
|
|
733
|
+
const response = await this.http.delete(`/api/apps/${appId}/collections/${collection}`);
|
|
734
|
+
return response.data;
|
|
735
|
+
} catch (error) {
|
|
736
|
+
throw error instanceof OnChainDBError ? error :
|
|
737
|
+
new OnChainDBError('Failed to delete collection', 'COLLECTION_ERROR');
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
async updateCollection(collection: string, updates: UpdateCollectionRequest): Promise<any> {
|
|
742
|
+
const appId = this.config.appId;
|
|
743
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
744
|
+
try {
|
|
745
|
+
const response = await this.http.patch(`/api/apps/${appId}/collections/${collection}`, updates);
|
|
746
|
+
return response.data;
|
|
747
|
+
} catch (error) {
|
|
748
|
+
throw error instanceof OnChainDBError ? error :
|
|
749
|
+
new OnChainDBError('Failed to update collection', 'COLLECTION_ERROR');
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
async getRetention(collection: string): Promise<RetentionConfig> {
|
|
754
|
+
const appId = this.config.appId;
|
|
755
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
756
|
+
try {
|
|
757
|
+
const response = await this.http.get(`/api/apps/${appId}/collections/${collection}/retention`);
|
|
758
|
+
return response.data;
|
|
759
|
+
} catch (error) {
|
|
760
|
+
throw error instanceof OnChainDBError ? error :
|
|
761
|
+
new OnChainDBError('Failed to get retention config', 'RETENTION_ERROR');
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
async setRetention(collection: string, request: SetRetentionRequest): Promise<{ message: string; config: RetentionConfig }> {
|
|
766
|
+
const appId = this.config.appId;
|
|
767
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
768
|
+
try {
|
|
769
|
+
const response = await this.http.post(`/api/apps/${appId}/collections/${collection}/retention`, request);
|
|
770
|
+
return response.data;
|
|
771
|
+
} catch (error) {
|
|
772
|
+
throw error instanceof OnChainDBError ? error :
|
|
773
|
+
new OnChainDBError('Failed to set retention config', 'RETENTION_ERROR');
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
async getRetentionCost(): Promise<RetentionCostResponse> {
|
|
778
|
+
const appId = this.config.appId;
|
|
779
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
780
|
+
try {
|
|
781
|
+
const response = await this.http.get(`/api/apps/${appId}/retention/cost`);
|
|
782
|
+
return response.data;
|
|
783
|
+
} catch (error) {
|
|
784
|
+
throw error instanceof OnChainDBError ? error :
|
|
785
|
+
new OnChainDBError('Failed to get retention cost', 'RETENTION_ERROR');
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
async sql(query: string, options?: { includeHistory?: boolean }): Promise<SqlQueryResponse> {
|
|
790
|
+
try {
|
|
791
|
+
const body: SqlQueryRequest = { sql: query };
|
|
792
|
+
if (options?.includeHistory !== undefined) body.include_history = options.includeHistory;
|
|
793
|
+
const response = await this.http.post('/query/sql', body);
|
|
794
|
+
return response.data;
|
|
795
|
+
} catch (error) {
|
|
796
|
+
throw error instanceof OnChainDBError ? error :
|
|
797
|
+
new OnChainDBError('SQL query failed', 'SQL_ERROR');
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
async sqlInsert(sql: string): Promise<any> {
|
|
802
|
+
try {
|
|
803
|
+
const body: SqlInsertRequest = { sql };
|
|
804
|
+
const response = await this.http.post('/insert/sql', body);
|
|
805
|
+
return response.data;
|
|
806
|
+
} catch (error) {
|
|
807
|
+
throw error instanceof OnChainDBError ? error :
|
|
808
|
+
new OnChainDBError('SQL insert failed', 'SQL_ERROR');
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
async listQueries(): Promise<QueryDefinition[]> {
|
|
813
|
+
const appId = this.config.appId;
|
|
814
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
815
|
+
try {
|
|
816
|
+
const response = await this.http.get(`/apps/${appId}/queries`);
|
|
817
|
+
return response.data.queries ?? response.data;
|
|
818
|
+
} catch (error) {
|
|
819
|
+
throw error instanceof OnChainDBError ? error :
|
|
820
|
+
new OnChainDBError('Failed to list predefined queries', 'QUERY_ERROR');
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
async createQuery(request: CreateQueryRequest): Promise<CreateQueryResponse> {
|
|
825
|
+
const appId = this.config.appId;
|
|
826
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
827
|
+
try {
|
|
828
|
+
const response = await this.http.post(`/apps/${appId}/queries`, request);
|
|
829
|
+
return response.data;
|
|
830
|
+
} catch (error) {
|
|
831
|
+
throw error instanceof OnChainDBError ? error :
|
|
832
|
+
new OnChainDBError('Failed to create predefined query', 'QUERY_ERROR');
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
async getQuery(name: string): Promise<QueryDefinition> {
|
|
837
|
+
const appId = this.config.appId;
|
|
838
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
839
|
+
try {
|
|
840
|
+
const response = await this.http.get(`/apps/${appId}/queries/${name}`);
|
|
841
|
+
return response.data;
|
|
842
|
+
} catch (error) {
|
|
843
|
+
throw error instanceof OnChainDBError ? error :
|
|
844
|
+
new OnChainDBError('Failed to get predefined query', 'QUERY_ERROR');
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
async deleteQuery(name: string): Promise<{ success: boolean }> {
|
|
849
|
+
const appId = this.config.appId;
|
|
850
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
851
|
+
try {
|
|
852
|
+
const response = await this.http.delete(`/apps/${appId}/queries/${name}`);
|
|
853
|
+
return response.data;
|
|
854
|
+
} catch (error) {
|
|
855
|
+
throw error instanceof OnChainDBError ? error :
|
|
856
|
+
new OnChainDBError('Failed to delete predefined query', 'QUERY_ERROR');
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async executeQuery(name: string, params?: Record<string, string>, version?: number): Promise<QueryDataResponse> {
|
|
861
|
+
const appId = this.config.appId;
|
|
862
|
+
if (!appId) throw new ValidationError('appId must be configured');
|
|
863
|
+
try {
|
|
864
|
+
const queryParams: Record<string, string | number> = { ...params };
|
|
865
|
+
if (version !== undefined) queryParams['v'] = version;
|
|
866
|
+
const response = await this.http.get(`/api/queries/${appId}/${name}/data`, { params: queryParams });
|
|
867
|
+
return response.data;
|
|
868
|
+
} catch (error) {
|
|
869
|
+
throw error instanceof OnChainDBError ? error :
|
|
870
|
+
new OnChainDBError('Failed to execute predefined query', 'QUERY_ERROR');
|
|
871
|
+
}
|
|
872
872
|
}
|
|
873
873
|
|
|
874
874
|
/**
|
|
@@ -877,14 +877,14 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
877
877
|
* @param collection - Collection name
|
|
878
878
|
* @returns Collection information
|
|
879
879
|
*/
|
|
880
|
-
async getCollectionInfo(
|
|
880
|
+
async getCollectionInfo(collectionId: string): Promise<CollectionResponse> {
|
|
881
881
|
const appId = this.config.appId;
|
|
882
882
|
if (!appId) {
|
|
883
883
|
throw new ValidationError('appId must be configured');
|
|
884
884
|
}
|
|
885
885
|
|
|
886
886
|
try {
|
|
887
|
-
const response = await this.http.get(`/api/apps/${appId}/collections/${
|
|
887
|
+
const response = await this.http.get(`/api/apps/${appId}/collections/${collectionId}`);
|
|
888
888
|
return response.data;
|
|
889
889
|
} catch (error) {
|
|
890
890
|
throw error instanceof OnChainDBError ? error :
|
|
@@ -985,62 +985,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
985
985
|
}
|
|
986
986
|
}
|
|
987
987
|
|
|
988
|
-
/**
|
|
989
|
-
* Execute a simple query with basic filtering
|
|
990
|
-
*
|
|
991
|
-
* @param request - Simple query request
|
|
992
|
-
* @returns Query response with records
|
|
993
|
-
*
|
|
994
|
-
* @example
|
|
995
|
-
* ```typescript
|
|
996
|
-
* const tweets = await db.query({
|
|
997
|
-
* collection: 'tweets',
|
|
998
|
-
* filters: { author: 'alice' },
|
|
999
|
-
* limit: 10
|
|
1000
|
-
* });
|
|
1001
|
-
* ```
|
|
1002
|
-
*/
|
|
1003
|
-
async query(request: {
|
|
1004
|
-
collection: string;
|
|
1005
|
-
filters?: Record<string, any>;
|
|
1006
|
-
limit?: number;
|
|
1007
|
-
offset?: number;
|
|
1008
|
-
sort?: string[];
|
|
1009
|
-
}): Promise<QueryResponse> {
|
|
1010
|
-
try {
|
|
1011
|
-
const queryBuilder = this.queryBuilder();
|
|
1012
|
-
|
|
1013
|
-
// Set collection using root building
|
|
1014
|
-
const root = this.resolveRoot(request);
|
|
1015
|
-
|
|
1016
|
-
// Add filters if provided
|
|
1017
|
-
if (request.filters) {
|
|
1018
|
-
queryBuilder.find(builder => {
|
|
1019
|
-
const conditions = Object.entries(request.filters!).map(([field, value]) =>
|
|
1020
|
-
LogicalOperator.Condition(builder.field(field).equals(value))
|
|
1021
|
-
);
|
|
1022
|
-
return conditions.length === 1 ? conditions[0] : LogicalOperator.And(conditions);
|
|
1023
|
-
});
|
|
1024
|
-
}
|
|
1025
|
-
|
|
1026
|
-
// Set limit and offset
|
|
1027
|
-
if (request.limit) {
|
|
1028
|
-
queryBuilder.limit(request.limit);
|
|
1029
|
-
}
|
|
1030
|
-
|
|
1031
|
-
if (request.offset) {
|
|
1032
|
-
queryBuilder.offset(request.offset);
|
|
1033
|
-
}
|
|
1034
|
-
|
|
1035
|
-
// Execute the query
|
|
1036
|
-
return await queryBuilder.execute();
|
|
1037
|
-
|
|
1038
|
-
} catch (error) {
|
|
1039
|
-
throw error instanceof OnChainDBError ? error :
|
|
1040
|
-
new OnChainDBError('Failed to execute query', 'QUERY_ERROR');
|
|
1041
|
-
}
|
|
1042
|
-
}
|
|
1043
|
-
|
|
1044
988
|
/**
|
|
1045
989
|
* Create a fluent query builder instance
|
|
1046
990
|
*
|
|
@@ -1050,10 +994,10 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1050
994
|
* ```typescript
|
|
1051
995
|
* const results = await db.queryBuilder()
|
|
1052
996
|
* .find(builder =>
|
|
1053
|
-
*
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
997
|
+
* builder.and(
|
|
998
|
+
* builder.field('status').equals('published'),
|
|
999
|
+
* builder.field('author').equals('alice')
|
|
1000
|
+
* )
|
|
1057
1001
|
* )
|
|
1058
1002
|
* .select(selection =>
|
|
1059
1003
|
* selection.field('title').field('content')
|
|
@@ -1069,359 +1013,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1069
1013
|
return new QueryBuilder(httpClient, this.config.endpoint, this.config.appId);
|
|
1070
1014
|
}
|
|
1071
1015
|
|
|
1072
|
-
/**
|
|
1073
|
-
* Create a batch operations instance for this client
|
|
1074
|
-
*
|
|
1075
|
-
* @returns BatchOperations instance
|
|
1076
|
-
*
|
|
1077
|
-
* @example
|
|
1078
|
-
* ```typescript
|
|
1079
|
-
* const batch = db.batch();
|
|
1080
|
-
* const results = await batch.store([...], { concurrency: 5 });
|
|
1081
|
-
* ```
|
|
1082
|
-
*/
|
|
1083
|
-
batch() {
|
|
1084
|
-
const {BatchOperations} = require('./batch');
|
|
1085
|
-
return new BatchOperations(this);
|
|
1086
|
-
}
|
|
1087
|
-
|
|
1088
|
-
/**
|
|
1089
|
-
* Find a single document by query (Prisma-style findUnique)
|
|
1090
|
-
* Returns the latest record by metadata (updatedAt or createdAt) if multiple matches.
|
|
1091
|
-
*
|
|
1092
|
-
* @param collection - Collection name to search in
|
|
1093
|
-
* @param where - Query conditions as key-value pairs
|
|
1094
|
-
* @returns Promise resolving to document or null if not found
|
|
1095
|
-
*
|
|
1096
|
-
* @example
|
|
1097
|
-
* ```typescript
|
|
1098
|
-
* const user = await client.findUnique('users', { email: 'alice@example.com' });
|
|
1099
|
-
* if (user) {
|
|
1100
|
-
* console.log('Found user:', user);
|
|
1101
|
-
* }
|
|
1102
|
-
* ```
|
|
1103
|
-
*/
|
|
1104
|
-
async findUnique<T extends Record<string, any>>(
|
|
1105
|
-
collection: string,
|
|
1106
|
-
where: Record<string, any>
|
|
1107
|
-
): Promise<T | null> {
|
|
1108
|
-
try {
|
|
1109
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1110
|
-
|
|
1111
|
-
// Add each where condition
|
|
1112
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1113
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1114
|
-
}
|
|
1115
|
-
|
|
1116
|
-
// Execute query and return the latest record by metadata
|
|
1117
|
-
return await queryBuilder.selectAll().executeUnique<T>();
|
|
1118
|
-
} catch (error) {
|
|
1119
|
-
console.error(`findUnique error for ${collection}:`, error);
|
|
1120
|
-
return null;
|
|
1121
|
-
}
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
// ===== PRISMA-LIKE CRUD OPERATIONS =====
|
|
1125
|
-
|
|
1126
|
-
/**
|
|
1127
|
-
* Find multiple documents by query (Prisma-style findMany)
|
|
1128
|
-
*
|
|
1129
|
-
* @param collection - Collection name to search in
|
|
1130
|
-
* @param where - Query conditions as key-value pairs
|
|
1131
|
-
* @param options - Query options (limit, offset, sort)
|
|
1132
|
-
* @returns Promise resolving to array of documents
|
|
1133
|
-
*
|
|
1134
|
-
* @example
|
|
1135
|
-
* ```typescript
|
|
1136
|
-
* const users = await client.findMany('users',
|
|
1137
|
-
* { active: true },
|
|
1138
|
-
* { limit: 10, sort: { field: 'createdAt', order: 'desc' } }
|
|
1139
|
-
* );
|
|
1140
|
-
* ```
|
|
1141
|
-
*/
|
|
1142
|
-
async findMany<T>(
|
|
1143
|
-
collection: string,
|
|
1144
|
-
where: Record<string, any> = {},
|
|
1145
|
-
options: {
|
|
1146
|
-
limit?: number;
|
|
1147
|
-
offset?: number;
|
|
1148
|
-
sort?: { field: string; order: 'asc' | 'desc' };
|
|
1149
|
-
} = {}
|
|
1150
|
-
): Promise<T[]> {
|
|
1151
|
-
try {
|
|
1152
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1153
|
-
|
|
1154
|
-
// Add where conditions
|
|
1155
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1156
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1157
|
-
}
|
|
1158
|
-
|
|
1159
|
-
// Add limit
|
|
1160
|
-
if (options.limit) {
|
|
1161
|
-
queryBuilder = queryBuilder.limit(options.limit);
|
|
1162
|
-
}
|
|
1163
|
-
|
|
1164
|
-
// Add offset
|
|
1165
|
-
if (options.offset) {
|
|
1166
|
-
queryBuilder = queryBuilder.offset(options.offset);
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
const result = await queryBuilder.selectAll().execute();
|
|
1170
|
-
|
|
1171
|
-
if (!result.records) {
|
|
1172
|
-
return [];
|
|
1173
|
-
}
|
|
1174
|
-
|
|
1175
|
-
// Apply sorting if specified (client-side for now)
|
|
1176
|
-
let records = result.records as T[];
|
|
1177
|
-
if (options.sort) {
|
|
1178
|
-
records = records.sort((a: any, b: any) => {
|
|
1179
|
-
const aVal = a[options.sort!.field];
|
|
1180
|
-
const bVal = b[options.sort!.field];
|
|
1181
|
-
|
|
1182
|
-
if (options.sort!.order === 'asc') {
|
|
1183
|
-
return aVal > bVal ? 1 : -1;
|
|
1184
|
-
} else {
|
|
1185
|
-
return aVal < bVal ? 1 : -1;
|
|
1186
|
-
}
|
|
1187
|
-
});
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
return records;
|
|
1191
|
-
} catch (error) {
|
|
1192
|
-
console.error(`findMany error for ${collection}:`, error);
|
|
1193
|
-
return [];
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
/**
|
|
1198
|
-
* Create a new document (Prisma-style create)
|
|
1199
|
-
*
|
|
1200
|
-
* @param collection - Collection name
|
|
1201
|
-
* @param data - Document data (without id, createdAt, updatedAt)
|
|
1202
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1203
|
-
* @param options - Optional settings (idGenerator)
|
|
1204
|
-
* @returns Promise resolving to created document
|
|
1205
|
-
*
|
|
1206
|
-
* @example
|
|
1207
|
-
* ```typescript
|
|
1208
|
-
* const user = await client.createDocument('users',
|
|
1209
|
-
* { email: 'alice@example.com', name: 'Alice' },
|
|
1210
|
-
* async (quote) => {
|
|
1211
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1212
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1213
|
-
* }
|
|
1214
|
-
* );
|
|
1215
|
-
* ```
|
|
1216
|
-
*/
|
|
1217
|
-
async createDocument<T extends Record<string, any>>(
|
|
1218
|
-
collection: string,
|
|
1219
|
-
data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>,
|
|
1220
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>,
|
|
1221
|
-
options?: {
|
|
1222
|
-
idGenerator?: () => string;
|
|
1223
|
-
}
|
|
1224
|
-
): Promise<T> {
|
|
1225
|
-
const document: any = {
|
|
1226
|
-
id: options?.idGenerator ? options.idGenerator() : this.generateId(),
|
|
1227
|
-
...data,
|
|
1228
|
-
createdAt: new Date().toISOString(),
|
|
1229
|
-
updatedAt: new Date().toISOString(),
|
|
1230
|
-
};
|
|
1231
|
-
|
|
1232
|
-
// Use the store method with x402 payment callback
|
|
1233
|
-
await this.store({
|
|
1234
|
-
collection,
|
|
1235
|
-
data: [document],
|
|
1236
|
-
}, paymentCallback);
|
|
1237
|
-
|
|
1238
|
-
return document as T;
|
|
1239
|
-
}
|
|
1240
|
-
|
|
1241
|
-
/**
|
|
1242
|
-
* Update an existing document (Prisma-style update)
|
|
1243
|
-
*
|
|
1244
|
-
* @param collection - Collection name
|
|
1245
|
-
* @param where - Query conditions to find document
|
|
1246
|
-
* @param data - Partial data to update
|
|
1247
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1248
|
-
* @returns Promise resolving to updated document or null if not found
|
|
1249
|
-
*
|
|
1250
|
-
* @example
|
|
1251
|
-
* ```typescript
|
|
1252
|
-
* const updated = await client.updateDocument('users',
|
|
1253
|
-
* { email: 'alice@example.com' },
|
|
1254
|
-
* { name: 'Alice Smith' },
|
|
1255
|
-
* async (quote) => {
|
|
1256
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1257
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1258
|
-
* }
|
|
1259
|
-
* );
|
|
1260
|
-
* ```
|
|
1261
|
-
*/
|
|
1262
|
-
async updateDocument<T extends Record<string, any>>(
|
|
1263
|
-
collection: string,
|
|
1264
|
-
where: Record<string, any>,
|
|
1265
|
-
data: Partial<T>,
|
|
1266
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>
|
|
1267
|
-
): Promise<T | null> {
|
|
1268
|
-
// Fetch current document
|
|
1269
|
-
const current = await this.findUnique<T>(collection, where);
|
|
1270
|
-
|
|
1271
|
-
if (!current) {
|
|
1272
|
-
return null;
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
// Create updated document
|
|
1276
|
-
const updated: any = {
|
|
1277
|
-
...current,
|
|
1278
|
-
...data,
|
|
1279
|
-
updatedAt: new Date().toISOString(),
|
|
1280
|
-
};
|
|
1281
|
-
|
|
1282
|
-
// Store updated version with x402 payment callback
|
|
1283
|
-
await this.store({
|
|
1284
|
-
collection,
|
|
1285
|
-
data: [updated],
|
|
1286
|
-
}, paymentCallback);
|
|
1287
|
-
|
|
1288
|
-
return updated as T;
|
|
1289
|
-
}
|
|
1290
|
-
|
|
1291
|
-
/**
|
|
1292
|
-
* Upsert a document (Prisma-style upsert - create or update)
|
|
1293
|
-
*
|
|
1294
|
-
* @param collection - Collection name
|
|
1295
|
-
* @param where - Query conditions to find document
|
|
1296
|
-
* @param create - Data for creating new document
|
|
1297
|
-
* @param update - Data for updating existing document
|
|
1298
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1299
|
-
* @param options - Optional settings (idGenerator)
|
|
1300
|
-
* @returns Promise resolving to created/updated document
|
|
1301
|
-
*
|
|
1302
|
-
* @example
|
|
1303
|
-
* ```typescript
|
|
1304
|
-
* const user = await client.upsertDocument('users',
|
|
1305
|
-
* { email: 'alice@example.com' },
|
|
1306
|
-
* { email: 'alice@example.com', name: 'Alice', active: true },
|
|
1307
|
-
* { active: true },
|
|
1308
|
-
* async (quote) => {
|
|
1309
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1310
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1311
|
-
* }
|
|
1312
|
-
* );
|
|
1313
|
-
* ```
|
|
1314
|
-
*/
|
|
1315
|
-
async upsertDocument<T extends Record<string, any>>(
|
|
1316
|
-
collection: string,
|
|
1317
|
-
where: Record<string, any>,
|
|
1318
|
-
create: Omit<T, 'id' | 'createdAt' | 'updatedAt'>,
|
|
1319
|
-
update: Partial<T>,
|
|
1320
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>,
|
|
1321
|
-
options?: {
|
|
1322
|
-
idGenerator?: () => string;
|
|
1323
|
-
}
|
|
1324
|
-
): Promise<T> {
|
|
1325
|
-
const existing = await this.findUnique<T>(collection, where);
|
|
1326
|
-
|
|
1327
|
-
if (existing) {
|
|
1328
|
-
return (await this.updateDocument<T>(collection, where, update, paymentCallback))!;
|
|
1329
|
-
} else {
|
|
1330
|
-
return await this.createDocument<T>(collection, create, paymentCallback, options);
|
|
1331
|
-
}
|
|
1332
|
-
}
|
|
1333
|
-
|
|
1334
|
-
/**
|
|
1335
|
-
* Soft delete a document by marking it as deleted
|
|
1336
|
-
*
|
|
1337
|
-
* @param collection - Collection name
|
|
1338
|
-
* @param where - Query conditions to find document
|
|
1339
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1340
|
-
* @returns Promise resolving to true if deleted, false if not found
|
|
1341
|
-
*
|
|
1342
|
-
* @example
|
|
1343
|
-
* ```typescript
|
|
1344
|
-
* const deleted = await client.deleteDocument('users',
|
|
1345
|
-
* { email: 'alice@example.com' },
|
|
1346
|
-
* async (quote) => {
|
|
1347
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1348
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1349
|
-
* }
|
|
1350
|
-
* );
|
|
1351
|
-
* ```
|
|
1352
|
-
*/
|
|
1353
|
-
async deleteDocument<T extends Record<string, any>>(
|
|
1354
|
-
collection: string,
|
|
1355
|
-
where: Record<string, any>,
|
|
1356
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>
|
|
1357
|
-
): Promise<boolean> {
|
|
1358
|
-
const existing = await this.findUnique<T>(collection, where);
|
|
1359
|
-
|
|
1360
|
-
if (!existing) {
|
|
1361
|
-
return false;
|
|
1362
|
-
}
|
|
1363
|
-
|
|
1364
|
-
// Soft delete by marking
|
|
1365
|
-
const deleted: any = {
|
|
1366
|
-
...existing,
|
|
1367
|
-
deleted: true,
|
|
1368
|
-
updatedAt: new Date().toISOString(),
|
|
1369
|
-
};
|
|
1370
|
-
|
|
1371
|
-
// Store deleted version with x402 payment callback
|
|
1372
|
-
await this.store({
|
|
1373
|
-
collection,
|
|
1374
|
-
data: [deleted],
|
|
1375
|
-
}, paymentCallback);
|
|
1376
|
-
|
|
1377
|
-
return true;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
/**
|
|
1381
|
-
* Count documents matching criteria
|
|
1382
|
-
* Uses server-side aggregation via QueryBuilder for efficiency.
|
|
1383
|
-
*
|
|
1384
|
-
* @param collection - Collection name
|
|
1385
|
-
* @param where - Query conditions
|
|
1386
|
-
* @returns Promise resolving to count
|
|
1387
|
-
*
|
|
1388
|
-
* @example
|
|
1389
|
-
* ```typescript
|
|
1390
|
-
* const activeUsers = await client.countDocuments('users', { active: true });
|
|
1391
|
-
* console.log(`Active users: ${activeUsers}`);
|
|
1392
|
-
* ```
|
|
1393
|
-
*/
|
|
1394
|
-
async countDocuments(collection: string, where: Record<string, any> = {}): Promise<number> {
|
|
1395
|
-
try {
|
|
1396
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1397
|
-
|
|
1398
|
-
// Add where conditions
|
|
1399
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1400
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1401
|
-
}
|
|
1402
|
-
|
|
1403
|
-
return await queryBuilder.count();
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
console.error(`countDocuments error for ${collection}:`, error);
|
|
1406
|
-
return 0;
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
|
|
1410
|
-
/**
|
|
1411
|
-
* Generate a unique ID for documents (simple base62 implementation)
|
|
1412
|
-
* Override this if you want to use a different ID generation strategy
|
|
1413
|
-
*
|
|
1414
|
-
* @returns Unique ID string
|
|
1415
|
-
*/
|
|
1416
|
-
generateId(): string {
|
|
1417
|
-
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
1418
|
-
let id = '';
|
|
1419
|
-
for (let i = 0; i < 24; i++) {
|
|
1420
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
1421
|
-
}
|
|
1422
|
-
return id;
|
|
1423
|
-
}
|
|
1424
|
-
|
|
1425
1016
|
/**
|
|
1426
1017
|
* Get task status by ticket ID
|
|
1427
1018
|
* @param ticketId - The ticket ID returned from async store operation
|
|
@@ -1574,13 +1165,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1574
1165
|
return await this.http.post<UploadBlobResponse>(
|
|
1575
1166
|
`/api/apps/${appId}/blobs/${request.collection}`,
|
|
1576
1167
|
formData,
|
|
1577
|
-
{
|
|
1578
|
-
headers: {
|
|
1579
|
-
'Content-Type': 'multipart/form-data',
|
|
1580
|
-
'X-App-Key': this.config.appKey,
|
|
1581
|
-
...headers
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1168
|
+
{ headers: { 'Content-Type': 'multipart/form-data', ...headers } }
|
|
1584
1169
|
);
|
|
1585
1170
|
};
|
|
1586
1171
|
|
|
@@ -1665,12 +1250,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1665
1250
|
// Retrieve blob via GET endpoint
|
|
1666
1251
|
const response = await this.http.get(
|
|
1667
1252
|
`/api/apps/${appId}/blobs/${request.collection}/${request.blob_id}`,
|
|
1668
|
-
{
|
|
1669
|
-
responseType: 'arraybuffer',
|
|
1670
|
-
headers: {
|
|
1671
|
-
'X-App-Key': this.config.appKey
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1253
|
+
{ responseType: 'arraybuffer' }
|
|
1674
1254
|
);
|
|
1675
1255
|
|
|
1676
1256
|
// Return as Blob in browser, Buffer in Node.js
|
|
@@ -1688,49 +1268,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1688
1268
|
}
|
|
1689
1269
|
}
|
|
1690
1270
|
|
|
1691
|
-
/**
|
|
1692
|
-
* Query blob metadata using the standard query interface
|
|
1693
|
-
*
|
|
1694
|
-
* Returns metadata about blobs without downloading the actual binary data.
|
|
1695
|
-
* Useful for listing, filtering, and searching blobs by their metadata.
|
|
1696
|
-
*
|
|
1697
|
-
* @param collection - Blob collection name
|
|
1698
|
-
* @param where - Query conditions for filtering blobs
|
|
1699
|
-
* @returns Promise resolving to array of blob metadata records
|
|
1700
|
-
*
|
|
1701
|
-
* @example
|
|
1702
|
-
* ```typescript
|
|
1703
|
-
* // Query all blobs by user
|
|
1704
|
-
* const userBlobs = await client.queryBlobMetadata('avatars', {
|
|
1705
|
-
* user_address: 'celestia1abc...'
|
|
1706
|
-
* });
|
|
1707
|
-
*
|
|
1708
|
-
* // Query blobs by content type
|
|
1709
|
-
* const images = await client.queryBlobMetadata('uploads', {
|
|
1710
|
-
* content_type: { $regex: 'image/' }
|
|
1711
|
-
* });
|
|
1712
|
-
*
|
|
1713
|
-
* // Query recent blobs
|
|
1714
|
-
* const recentBlobs = await client.queryBlobMetadata('files', {
|
|
1715
|
-
* uploaded_at: { $gte: '2024-01-01T00:00:00Z' }
|
|
1716
|
-
* });
|
|
1717
|
-
*
|
|
1718
|
-
* // Access blob metadata
|
|
1719
|
-
* for (const blob of userBlobs) {
|
|
1720
|
-
* console.log('Blob ID:', blob.blob_id);
|
|
1721
|
-
* console.log('Size:', blob.size_bytes);
|
|
1722
|
-
* console.log('Type:', blob.content_type);
|
|
1723
|
-
* console.log('Custom fields:', blob);
|
|
1724
|
-
* }
|
|
1725
|
-
* ```
|
|
1726
|
-
*/
|
|
1727
|
-
async queryBlobMetadata(
|
|
1728
|
-
collection: string,
|
|
1729
|
-
where: Record<string, any> = {}
|
|
1730
|
-
): Promise<BlobMetadata[]> {
|
|
1731
|
-
return await this.findMany<BlobMetadata>(collection, where);
|
|
1732
|
-
}
|
|
1733
|
-
|
|
1734
1271
|
/**
|
|
1735
1272
|
* Get default index type based on field type
|
|
1736
1273
|
*/
|
|
@@ -1749,6 +1286,17 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1749
1286
|
}
|
|
1750
1287
|
}
|
|
1751
1288
|
|
|
1289
|
+
private buildStoreResult(raw: any): StoreResponse {
|
|
1290
|
+
return {
|
|
1291
|
+
id: raw.id,
|
|
1292
|
+
block_height: raw.celestia_height || 0,
|
|
1293
|
+
transaction_hash: raw.blob_id || '',
|
|
1294
|
+
celestia_height: raw.celestia_height || 0,
|
|
1295
|
+
namespace: raw.namespace || '',
|
|
1296
|
+
confirmed: raw.celestia_height > 0
|
|
1297
|
+
};
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1752
1300
|
private validateStoreRequest(request: StoreRequest): void {
|
|
1753
1301
|
// Validate that either root or collection is provided
|
|
1754
1302
|
if (!request.root && !request.collection) {
|
|
@@ -1785,67 +1333,17 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1785
1333
|
}
|
|
1786
1334
|
}
|
|
1787
1335
|
|
|
1788
|
-
private handleHttpError(error: AxiosError): OnChainDBError {
|
|
1789
|
-
console.error(error);
|
|
1790
|
-
if (error.response) {
|
|
1791
|
-
const statusCode = error.response.status;
|
|
1792
|
-
const message = (error.response.data as any)?.error || error.message;
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
if (statusCode >= 400 && statusCode < 500) {
|
|
1796
|
-
return new ValidationError(message, error.response.data);
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
return new OnChainDBError(
|
|
1800
|
-
message,
|
|
1801
|
-
'HTTP_ERROR',
|
|
1802
|
-
statusCode,
|
|
1803
|
-
error.response.data
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
|
|
1807
|
-
if (error.request) {
|
|
1808
|
-
return new OnChainDBError(
|
|
1809
|
-
'Network error - could not reach OnChainDB service',
|
|
1810
|
-
'NETWORK_ERROR'
|
|
1811
|
-
);
|
|
1812
|
-
}
|
|
1813
|
-
|
|
1814
|
-
return new OnChainDBError(error.message, 'UNKNOWN_ERROR');
|
|
1815
|
-
}
|
|
1816
|
-
|
|
1817
1336
|
private sleep(ms: number): Promise<void> {
|
|
1818
1337
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1819
1338
|
}
|
|
1820
1339
|
|
|
1821
|
-
/**
|
|
1822
|
-
* Build root string from collection name using configured appId
|
|
1823
|
-
*
|
|
1824
|
-
* @param collection - Collection name
|
|
1825
|
-
* @returns Full root string in format "appId::collection" or just collection for system ops
|
|
1826
|
-
*/
|
|
1827
|
-
private buildRoot(collection: string): string {
|
|
1828
|
-
if (!this.config.appId) {
|
|
1829
|
-
return collection; // System operation or no appId configured
|
|
1830
|
-
}
|
|
1831
|
-
return `${this.config.appId}::${collection}`;
|
|
1832
|
-
}
|
|
1833
|
-
|
|
1834
|
-
/**
|
|
1835
|
-
* Resolve root parameter from request, building it if needed
|
|
1836
|
-
*
|
|
1837
|
-
* @param request - Store or Query request
|
|
1838
|
-
* @returns Resolved root string
|
|
1839
|
-
*/
|
|
1840
1340
|
private resolveRoot(request: { root?: string; collection?: string }): string {
|
|
1841
1341
|
if (request.root) {
|
|
1842
|
-
return request.root;
|
|
1342
|
+
return request.root;
|
|
1843
1343
|
}
|
|
1844
|
-
|
|
1845
1344
|
if (request.collection) {
|
|
1846
|
-
return this.
|
|
1345
|
+
return this.config.appId ? `${this.config.appId}::${request.collection}` : request.collection;
|
|
1847
1346
|
}
|
|
1848
|
-
|
|
1849
1347
|
throw new ValidationError('Either root or collection must be provided');
|
|
1850
1348
|
}
|
|
1851
1349
|
|