@onchaindb/sdk 0.4.5 → 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 +198 -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 +242 -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) {
|
|
@@ -497,98 +461,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
497
461
|
}
|
|
498
462
|
}
|
|
499
463
|
|
|
500
|
-
/**
|
|
501
|
-
* Store data and return a promise that resolves when transaction is confirmed
|
|
502
|
-
*
|
|
503
|
-
* @param request - Store request
|
|
504
|
-
* @param paymentOptions - Payment configuration for broker fees
|
|
505
|
-
* @returns Promise resolving when transaction is confirmed on blockchain
|
|
506
|
-
*/
|
|
507
|
-
async storeAndConfirm(
|
|
508
|
-
request: StoreRequest
|
|
509
|
-
): Promise<StoreResponse> {
|
|
510
|
-
return this.store(request, undefined, true);
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Wait for transaction confirmation
|
|
515
|
-
*
|
|
516
|
-
* @param transactionHash - Transaction hash to monitor
|
|
517
|
-
* @param maxWaitTime - Maximum wait time in milliseconds (default: 5 minutes)
|
|
518
|
-
* @returns Promise resolving to confirmed transaction
|
|
519
|
-
*/
|
|
520
|
-
async waitForConfirmation(transactionHash: string, maxWaitTime: number = 300000): Promise<StoreResponse> {
|
|
521
|
-
const startTime = Date.now();
|
|
522
|
-
const pollInterval = 3000; // Poll every 3 seconds (same as server)
|
|
523
|
-
|
|
524
|
-
console.log(`🔄 Waiting for transaction ${transactionHash} confirmation...`);
|
|
525
|
-
|
|
526
|
-
while (Date.now() - startTime < maxWaitTime) {
|
|
527
|
-
try {
|
|
528
|
-
// Query Celestia RPC directly to check transaction status
|
|
529
|
-
const rpcUrl = 'https://celestia-mocha-rpc.publicnode.com:443'; // Default testnet RPC
|
|
530
|
-
const txUrl = `${rpcUrl}/tx?hash=0x${transactionHash}`;
|
|
531
|
-
|
|
532
|
-
console.log(`🔍 Checking transaction status: attempt ${Math.floor((Date.now() - startTime) / pollInterval) + 1}`);
|
|
533
|
-
|
|
534
|
-
const response = await axios.get(txUrl);
|
|
535
|
-
|
|
536
|
-
if (response.data?.result && response.data.result !== null) {
|
|
537
|
-
const txResult = response.data.result;
|
|
538
|
-
const height = parseInt(txResult.height);
|
|
539
|
-
|
|
540
|
-
if (height > 0) {
|
|
541
|
-
console.log(`✅ Transaction ${transactionHash} confirmed at height ${height}`);
|
|
542
|
-
|
|
543
|
-
const confirmedTx: StoreResponse = {
|
|
544
|
-
id: transactionHash, // Use transaction hash as ID for confirmation
|
|
545
|
-
namespace: '', // Will be filled by actual usage
|
|
546
|
-
block_height: height,
|
|
547
|
-
transaction_hash: transactionHash,
|
|
548
|
-
celestia_height: height,
|
|
549
|
-
confirmed: true
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
this.emit('transaction:confirmed', {
|
|
553
|
-
id: transactionHash,
|
|
554
|
-
status: 'confirmed',
|
|
555
|
-
block_height: height,
|
|
556
|
-
transaction_hash: transactionHash
|
|
557
|
-
});
|
|
558
|
-
return confirmedTx;
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
// Still pending, wait and retry
|
|
563
|
-
console.log(`⏳ Transaction still pending, waiting ${pollInterval}ms...`);
|
|
564
|
-
this.emit('transaction:pending', {
|
|
565
|
-
id: transactionHash,
|
|
566
|
-
status: 'pending',
|
|
567
|
-
block_height: 0,
|
|
568
|
-
transaction_hash: transactionHash
|
|
569
|
-
});
|
|
570
|
-
await this.sleep(pollInterval);
|
|
571
|
-
|
|
572
|
-
} catch (error) {
|
|
573
|
-
// For 404 or other errors, the transaction might not be confirmed yet
|
|
574
|
-
if (Date.now() - startTime >= maxWaitTime) {
|
|
575
|
-
throw new TransactionError(
|
|
576
|
-
`Transaction confirmation timeout after ${maxWaitTime}ms`,
|
|
577
|
-
transactionHash
|
|
578
|
-
);
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
// Wait and retry for temporary errors
|
|
582
|
-
console.log(`⚠️ Error checking transaction (will retry): ${error}`);
|
|
583
|
-
await this.sleep(pollInterval);
|
|
584
|
-
}
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
throw new TransactionError(
|
|
588
|
-
`Transaction confirmation timeout after ${maxWaitTime}ms`,
|
|
589
|
-
transactionHash
|
|
590
|
-
);
|
|
591
|
-
}
|
|
592
464
|
|
|
593
465
|
/**
|
|
594
466
|
* Create an index on a collection field
|
|
@@ -752,7 +624,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
752
624
|
* console.log('Removed:', result.removed); // [{ field: 'username', type: 'string' }]
|
|
753
625
|
* ```
|
|
754
626
|
*/
|
|
755
|
-
async syncCollection(schema:
|
|
627
|
+
async syncCollection(schema: SimpleCollectionSchemaWithSharding): Promise<SyncCollectionResult> {
|
|
756
628
|
const appId = this.config.appId;
|
|
757
629
|
if (!appId) {
|
|
758
630
|
throw new ValidationError('appId must be configured to sync collections');
|
|
@@ -767,22 +639,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
767
639
|
errors: []
|
|
768
640
|
};
|
|
769
641
|
|
|
770
|
-
// Get existing indexes for this collection
|
|
771
|
-
let existingIndexes: Array<{ field_name: string; index_type: string; name: string }> = [];
|
|
772
|
-
try {
|
|
773
|
-
const response = await this.http.get(`/api/apps/${appId}/collections/${schema.name}/indexes`);
|
|
774
|
-
existingIndexes = response.data?.indexes || response.data || [];
|
|
775
|
-
} catch (error) {
|
|
776
|
-
// Collection might not exist yet, that's okay
|
|
777
|
-
existingIndexes = [];
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Build map of existing indexes by field name
|
|
781
|
-
const existingByField = new Map<string, { type: string; name: string }>();
|
|
782
|
-
for (const idx of existingIndexes) {
|
|
783
|
-
existingByField.set(idx.field_name, {type: idx.index_type, name: idx.name});
|
|
784
|
-
}
|
|
785
|
-
|
|
786
642
|
// Merge base fields if enabled (default: true)
|
|
787
643
|
const allFields: Record<string, SimpleFieldDefinition> = {};
|
|
788
644
|
if (schema.useBaseFields !== false) {
|
|
@@ -798,9 +654,9 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
798
654
|
}
|
|
799
655
|
}
|
|
800
656
|
|
|
801
|
-
//
|
|
657
|
+
// Create all desired indexes (broker handles upsert)
|
|
802
658
|
for (const fieldName of desiredIndexedFields) {
|
|
803
|
-
|
|
659
|
+
{
|
|
804
660
|
const fieldDef = allFields[fieldName];
|
|
805
661
|
const indexType = fieldDef.indexType || this.getDefaultIndexType(fieldDef.type);
|
|
806
662
|
|
|
@@ -840,36 +696,179 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
840
696
|
}
|
|
841
697
|
}
|
|
842
698
|
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
field: fieldName,
|
|
852
|
-
type: existing.type
|
|
853
|
-
});
|
|
854
|
-
} catch (error) {
|
|
855
|
-
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
856
|
-
result.errors!.push(`Failed to remove index on ${fieldName}: ${errorMsg}`);
|
|
857
|
-
result.success = false;
|
|
858
|
-
}
|
|
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;
|
|
859
707
|
}
|
|
860
708
|
}
|
|
861
709
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
}
|
|
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');
|
|
870
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
|
+
}
|
|
871
728
|
|
|
872
|
-
|
|
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
|
+
}
|
|
873
872
|
}
|
|
874
873
|
|
|
875
874
|
/**
|
|
@@ -878,14 +877,14 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
878
877
|
* @param collection - Collection name
|
|
879
878
|
* @returns Collection information
|
|
880
879
|
*/
|
|
881
|
-
async getCollectionInfo(
|
|
880
|
+
async getCollectionInfo(collectionId: string): Promise<CollectionResponse> {
|
|
882
881
|
const appId = this.config.appId;
|
|
883
882
|
if (!appId) {
|
|
884
883
|
throw new ValidationError('appId must be configured');
|
|
885
884
|
}
|
|
886
885
|
|
|
887
886
|
try {
|
|
888
|
-
const response = await this.http.get(`/api/apps/${appId}/collections/${
|
|
887
|
+
const response = await this.http.get(`/api/apps/${appId}/collections/${collectionId}`);
|
|
889
888
|
return response.data;
|
|
890
889
|
} catch (error) {
|
|
891
890
|
throw error instanceof OnChainDBError ? error :
|
|
@@ -986,62 +985,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
986
985
|
}
|
|
987
986
|
}
|
|
988
987
|
|
|
989
|
-
/**
|
|
990
|
-
* Execute a simple query with basic filtering
|
|
991
|
-
*
|
|
992
|
-
* @param request - Simple query request
|
|
993
|
-
* @returns Query response with records
|
|
994
|
-
*
|
|
995
|
-
* @example
|
|
996
|
-
* ```typescript
|
|
997
|
-
* const tweets = await db.query({
|
|
998
|
-
* collection: 'tweets',
|
|
999
|
-
* filters: { author: 'alice' },
|
|
1000
|
-
* limit: 10
|
|
1001
|
-
* });
|
|
1002
|
-
* ```
|
|
1003
|
-
*/
|
|
1004
|
-
async query(request: {
|
|
1005
|
-
collection: string;
|
|
1006
|
-
filters?: Record<string, any>;
|
|
1007
|
-
limit?: number;
|
|
1008
|
-
offset?: number;
|
|
1009
|
-
sort?: string[];
|
|
1010
|
-
}): Promise<QueryResponse> {
|
|
1011
|
-
try {
|
|
1012
|
-
const queryBuilder = this.queryBuilder();
|
|
1013
|
-
|
|
1014
|
-
// Set collection using root building
|
|
1015
|
-
const root = this.resolveRoot(request);
|
|
1016
|
-
|
|
1017
|
-
// Add filters if provided
|
|
1018
|
-
if (request.filters) {
|
|
1019
|
-
queryBuilder.find(builder => {
|
|
1020
|
-
const conditions = Object.entries(request.filters!).map(([field, value]) =>
|
|
1021
|
-
LogicalOperator.Condition(builder.field(field).equals(value))
|
|
1022
|
-
);
|
|
1023
|
-
return conditions.length === 1 ? conditions[0] : LogicalOperator.And(conditions);
|
|
1024
|
-
});
|
|
1025
|
-
}
|
|
1026
|
-
|
|
1027
|
-
// Set limit and offset
|
|
1028
|
-
if (request.limit) {
|
|
1029
|
-
queryBuilder.limit(request.limit);
|
|
1030
|
-
}
|
|
1031
|
-
|
|
1032
|
-
if (request.offset) {
|
|
1033
|
-
queryBuilder.offset(request.offset);
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
// Execute the query
|
|
1037
|
-
return await queryBuilder.execute();
|
|
1038
|
-
|
|
1039
|
-
} catch (error) {
|
|
1040
|
-
throw error instanceof OnChainDBError ? error :
|
|
1041
|
-
new OnChainDBError('Failed to execute query', 'QUERY_ERROR');
|
|
1042
|
-
}
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
988
|
/**
|
|
1046
989
|
* Create a fluent query builder instance
|
|
1047
990
|
*
|
|
@@ -1051,10 +994,10 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1051
994
|
* ```typescript
|
|
1052
995
|
* const results = await db.queryBuilder()
|
|
1053
996
|
* .find(builder =>
|
|
1054
|
-
*
|
|
1055
|
-
*
|
|
1056
|
-
*
|
|
1057
|
-
*
|
|
997
|
+
* builder.and(
|
|
998
|
+
* builder.field('status').equals('published'),
|
|
999
|
+
* builder.field('author').equals('alice')
|
|
1000
|
+
* )
|
|
1058
1001
|
* )
|
|
1059
1002
|
* .select(selection =>
|
|
1060
1003
|
* selection.field('title').field('content')
|
|
@@ -1070,359 +1013,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1070
1013
|
return new QueryBuilder(httpClient, this.config.endpoint, this.config.appId);
|
|
1071
1014
|
}
|
|
1072
1015
|
|
|
1073
|
-
/**
|
|
1074
|
-
* Create a batch operations instance for this client
|
|
1075
|
-
*
|
|
1076
|
-
* @returns BatchOperations instance
|
|
1077
|
-
*
|
|
1078
|
-
* @example
|
|
1079
|
-
* ```typescript
|
|
1080
|
-
* const batch = db.batch();
|
|
1081
|
-
* const results = await batch.store([...], { concurrency: 5 });
|
|
1082
|
-
* ```
|
|
1083
|
-
*/
|
|
1084
|
-
batch() {
|
|
1085
|
-
const {BatchOperations} = require('./batch');
|
|
1086
|
-
return new BatchOperations(this);
|
|
1087
|
-
}
|
|
1088
|
-
|
|
1089
|
-
/**
|
|
1090
|
-
* Find a single document by query (Prisma-style findUnique)
|
|
1091
|
-
* Returns the latest record by metadata (updatedAt or createdAt) if multiple matches.
|
|
1092
|
-
*
|
|
1093
|
-
* @param collection - Collection name to search in
|
|
1094
|
-
* @param where - Query conditions as key-value pairs
|
|
1095
|
-
* @returns Promise resolving to document or null if not found
|
|
1096
|
-
*
|
|
1097
|
-
* @example
|
|
1098
|
-
* ```typescript
|
|
1099
|
-
* const user = await client.findUnique('users', { email: 'alice@example.com' });
|
|
1100
|
-
* if (user) {
|
|
1101
|
-
* console.log('Found user:', user);
|
|
1102
|
-
* }
|
|
1103
|
-
* ```
|
|
1104
|
-
*/
|
|
1105
|
-
async findUnique<T extends Record<string, any>>(
|
|
1106
|
-
collection: string,
|
|
1107
|
-
where: Record<string, any>
|
|
1108
|
-
): Promise<T | null> {
|
|
1109
|
-
try {
|
|
1110
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1111
|
-
|
|
1112
|
-
// Add each where condition
|
|
1113
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1114
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1115
|
-
}
|
|
1116
|
-
|
|
1117
|
-
// Execute query and return the latest record by metadata
|
|
1118
|
-
return await queryBuilder.selectAll().executeUnique<T>();
|
|
1119
|
-
} catch (error) {
|
|
1120
|
-
console.error(`findUnique error for ${collection}:`, error);
|
|
1121
|
-
return null;
|
|
1122
|
-
}
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// ===== PRISMA-LIKE CRUD OPERATIONS =====
|
|
1126
|
-
|
|
1127
|
-
/**
|
|
1128
|
-
* Find multiple documents by query (Prisma-style findMany)
|
|
1129
|
-
*
|
|
1130
|
-
* @param collection - Collection name to search in
|
|
1131
|
-
* @param where - Query conditions as key-value pairs
|
|
1132
|
-
* @param options - Query options (limit, offset, sort)
|
|
1133
|
-
* @returns Promise resolving to array of documents
|
|
1134
|
-
*
|
|
1135
|
-
* @example
|
|
1136
|
-
* ```typescript
|
|
1137
|
-
* const users = await client.findMany('users',
|
|
1138
|
-
* { active: true },
|
|
1139
|
-
* { limit: 10, sort: { field: 'createdAt', order: 'desc' } }
|
|
1140
|
-
* );
|
|
1141
|
-
* ```
|
|
1142
|
-
*/
|
|
1143
|
-
async findMany<T>(
|
|
1144
|
-
collection: string,
|
|
1145
|
-
where: Record<string, any> = {},
|
|
1146
|
-
options: {
|
|
1147
|
-
limit?: number;
|
|
1148
|
-
offset?: number;
|
|
1149
|
-
sort?: { field: string; order: 'asc' | 'desc' };
|
|
1150
|
-
} = {}
|
|
1151
|
-
): Promise<T[]> {
|
|
1152
|
-
try {
|
|
1153
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1154
|
-
|
|
1155
|
-
// Add where conditions
|
|
1156
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1157
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
// Add limit
|
|
1161
|
-
if (options.limit) {
|
|
1162
|
-
queryBuilder = queryBuilder.limit(options.limit);
|
|
1163
|
-
}
|
|
1164
|
-
|
|
1165
|
-
// Add offset
|
|
1166
|
-
if (options.offset) {
|
|
1167
|
-
queryBuilder = queryBuilder.offset(options.offset);
|
|
1168
|
-
}
|
|
1169
|
-
|
|
1170
|
-
const result = await queryBuilder.selectAll().execute();
|
|
1171
|
-
|
|
1172
|
-
if (!result.records) {
|
|
1173
|
-
return [];
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
// Apply sorting if specified (client-side for now)
|
|
1177
|
-
let records = result.records as T[];
|
|
1178
|
-
if (options.sort) {
|
|
1179
|
-
records = records.sort((a: any, b: any) => {
|
|
1180
|
-
const aVal = a[options.sort!.field];
|
|
1181
|
-
const bVal = b[options.sort!.field];
|
|
1182
|
-
|
|
1183
|
-
if (options.sort!.order === 'asc') {
|
|
1184
|
-
return aVal > bVal ? 1 : -1;
|
|
1185
|
-
} else {
|
|
1186
|
-
return aVal < bVal ? 1 : -1;
|
|
1187
|
-
}
|
|
1188
|
-
});
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
return records;
|
|
1192
|
-
} catch (error) {
|
|
1193
|
-
console.error(`findMany error for ${collection}:`, error);
|
|
1194
|
-
return [];
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
|
|
1198
|
-
/**
|
|
1199
|
-
* Create a new document (Prisma-style create)
|
|
1200
|
-
*
|
|
1201
|
-
* @param collection - Collection name
|
|
1202
|
-
* @param data - Document data (without id, createdAt, updatedAt)
|
|
1203
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1204
|
-
* @param options - Optional settings (idGenerator)
|
|
1205
|
-
* @returns Promise resolving to created document
|
|
1206
|
-
*
|
|
1207
|
-
* @example
|
|
1208
|
-
* ```typescript
|
|
1209
|
-
* const user = await client.createDocument('users',
|
|
1210
|
-
* { email: 'alice@example.com', name: 'Alice' },
|
|
1211
|
-
* async (quote) => {
|
|
1212
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1213
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1214
|
-
* }
|
|
1215
|
-
* );
|
|
1216
|
-
* ```
|
|
1217
|
-
*/
|
|
1218
|
-
async createDocument<T extends Record<string, any>>(
|
|
1219
|
-
collection: string,
|
|
1220
|
-
data: Omit<T, 'id' | 'createdAt' | 'updatedAt'>,
|
|
1221
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>,
|
|
1222
|
-
options?: {
|
|
1223
|
-
idGenerator?: () => string;
|
|
1224
|
-
}
|
|
1225
|
-
): Promise<T> {
|
|
1226
|
-
const document: any = {
|
|
1227
|
-
id: options?.idGenerator ? options.idGenerator() : this.generateId(),
|
|
1228
|
-
...data,
|
|
1229
|
-
createdAt: new Date().toISOString(),
|
|
1230
|
-
updatedAt: new Date().toISOString(),
|
|
1231
|
-
};
|
|
1232
|
-
|
|
1233
|
-
// Use the store method with x402 payment callback
|
|
1234
|
-
await this.store({
|
|
1235
|
-
collection,
|
|
1236
|
-
data: [document],
|
|
1237
|
-
}, paymentCallback);
|
|
1238
|
-
|
|
1239
|
-
return document as T;
|
|
1240
|
-
}
|
|
1241
|
-
|
|
1242
|
-
/**
|
|
1243
|
-
* Update an existing document (Prisma-style update)
|
|
1244
|
-
*
|
|
1245
|
-
* @param collection - Collection name
|
|
1246
|
-
* @param where - Query conditions to find document
|
|
1247
|
-
* @param data - Partial data to update
|
|
1248
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1249
|
-
* @returns Promise resolving to updated document or null if not found
|
|
1250
|
-
*
|
|
1251
|
-
* @example
|
|
1252
|
-
* ```typescript
|
|
1253
|
-
* const updated = await client.updateDocument('users',
|
|
1254
|
-
* { email: 'alice@example.com' },
|
|
1255
|
-
* { name: 'Alice Smith' },
|
|
1256
|
-
* async (quote) => {
|
|
1257
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1258
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1259
|
-
* }
|
|
1260
|
-
* );
|
|
1261
|
-
* ```
|
|
1262
|
-
*/
|
|
1263
|
-
async updateDocument<T extends Record<string, any>>(
|
|
1264
|
-
collection: string,
|
|
1265
|
-
where: Record<string, any>,
|
|
1266
|
-
data: Partial<T>,
|
|
1267
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>
|
|
1268
|
-
): Promise<T | null> {
|
|
1269
|
-
// Fetch current document
|
|
1270
|
-
const current = await this.findUnique<T>(collection, where);
|
|
1271
|
-
|
|
1272
|
-
if (!current) {
|
|
1273
|
-
return null;
|
|
1274
|
-
}
|
|
1275
|
-
|
|
1276
|
-
// Create updated document
|
|
1277
|
-
const updated: any = {
|
|
1278
|
-
...current,
|
|
1279
|
-
...data,
|
|
1280
|
-
updatedAt: new Date().toISOString(),
|
|
1281
|
-
};
|
|
1282
|
-
|
|
1283
|
-
// Store updated version with x402 payment callback
|
|
1284
|
-
await this.store({
|
|
1285
|
-
collection,
|
|
1286
|
-
data: [updated],
|
|
1287
|
-
}, paymentCallback);
|
|
1288
|
-
|
|
1289
|
-
return updated as T;
|
|
1290
|
-
}
|
|
1291
|
-
|
|
1292
|
-
/**
|
|
1293
|
-
* Upsert a document (Prisma-style upsert - create or update)
|
|
1294
|
-
*
|
|
1295
|
-
* @param collection - Collection name
|
|
1296
|
-
* @param where - Query conditions to find document
|
|
1297
|
-
* @param create - Data for creating new document
|
|
1298
|
-
* @param update - Data for updating existing document
|
|
1299
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1300
|
-
* @param options - Optional settings (idGenerator)
|
|
1301
|
-
* @returns Promise resolving to created/updated document
|
|
1302
|
-
*
|
|
1303
|
-
* @example
|
|
1304
|
-
* ```typescript
|
|
1305
|
-
* const user = await client.upsertDocument('users',
|
|
1306
|
-
* { email: 'alice@example.com' },
|
|
1307
|
-
* { email: 'alice@example.com', name: 'Alice', active: true },
|
|
1308
|
-
* { active: true },
|
|
1309
|
-
* async (quote) => {
|
|
1310
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1311
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1312
|
-
* }
|
|
1313
|
-
* );
|
|
1314
|
-
* ```
|
|
1315
|
-
*/
|
|
1316
|
-
async upsertDocument<T extends Record<string, any>>(
|
|
1317
|
-
collection: string,
|
|
1318
|
-
where: Record<string, any>,
|
|
1319
|
-
create: Omit<T, 'id' | 'createdAt' | 'updatedAt'>,
|
|
1320
|
-
update: Partial<T>,
|
|
1321
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>,
|
|
1322
|
-
options?: {
|
|
1323
|
-
idGenerator?: () => string;
|
|
1324
|
-
}
|
|
1325
|
-
): Promise<T> {
|
|
1326
|
-
const existing = await this.findUnique<T>(collection, where);
|
|
1327
|
-
|
|
1328
|
-
if (existing) {
|
|
1329
|
-
return (await this.updateDocument<T>(collection, where, update, paymentCallback))!;
|
|
1330
|
-
} else {
|
|
1331
|
-
return await this.createDocument<T>(collection, create, paymentCallback, options);
|
|
1332
|
-
}
|
|
1333
|
-
}
|
|
1334
|
-
|
|
1335
|
-
/**
|
|
1336
|
-
* Soft delete a document by marking it as deleted
|
|
1337
|
-
*
|
|
1338
|
-
* @param collection - Collection name
|
|
1339
|
-
* @param where - Query conditions to find document
|
|
1340
|
-
* @param paymentCallback - x402 payment callback for handling payment
|
|
1341
|
-
* @returns Promise resolving to true if deleted, false if not found
|
|
1342
|
-
*
|
|
1343
|
-
* @example
|
|
1344
|
-
* ```typescript
|
|
1345
|
-
* const deleted = await client.deleteDocument('users',
|
|
1346
|
-
* { email: 'alice@example.com' },
|
|
1347
|
-
* async (quote) => {
|
|
1348
|
-
* const txHash = await signAndBroadcastPayment(quote);
|
|
1349
|
-
* return { txHash, network: quote.network, sender: userAddress, chainType: quote.chainType, paymentMethod: quote.paymentMethod };
|
|
1350
|
-
* }
|
|
1351
|
-
* );
|
|
1352
|
-
* ```
|
|
1353
|
-
*/
|
|
1354
|
-
async deleteDocument<T extends Record<string, any>>(
|
|
1355
|
-
collection: string,
|
|
1356
|
-
where: Record<string, any>,
|
|
1357
|
-
paymentCallback?: (quote: X402Quote) => Promise<X402PaymentResult>
|
|
1358
|
-
): Promise<boolean> {
|
|
1359
|
-
const existing = await this.findUnique<T>(collection, where);
|
|
1360
|
-
|
|
1361
|
-
if (!existing) {
|
|
1362
|
-
return false;
|
|
1363
|
-
}
|
|
1364
|
-
|
|
1365
|
-
// Soft delete by marking
|
|
1366
|
-
const deleted: any = {
|
|
1367
|
-
...existing,
|
|
1368
|
-
deleted: true,
|
|
1369
|
-
updatedAt: new Date().toISOString(),
|
|
1370
|
-
};
|
|
1371
|
-
|
|
1372
|
-
// Store deleted version with x402 payment callback
|
|
1373
|
-
await this.store({
|
|
1374
|
-
collection,
|
|
1375
|
-
data: [deleted],
|
|
1376
|
-
}, paymentCallback);
|
|
1377
|
-
|
|
1378
|
-
return true;
|
|
1379
|
-
}
|
|
1380
|
-
|
|
1381
|
-
/**
|
|
1382
|
-
* Count documents matching criteria
|
|
1383
|
-
* Uses server-side aggregation via QueryBuilder for efficiency.
|
|
1384
|
-
*
|
|
1385
|
-
* @param collection - Collection name
|
|
1386
|
-
* @param where - Query conditions
|
|
1387
|
-
* @returns Promise resolving to count
|
|
1388
|
-
*
|
|
1389
|
-
* @example
|
|
1390
|
-
* ```typescript
|
|
1391
|
-
* const activeUsers = await client.countDocuments('users', { active: true });
|
|
1392
|
-
* console.log(`Active users: ${activeUsers}`);
|
|
1393
|
-
* ```
|
|
1394
|
-
*/
|
|
1395
|
-
async countDocuments(collection: string, where: Record<string, any> = {}): Promise<number> {
|
|
1396
|
-
try {
|
|
1397
|
-
let queryBuilder = this.queryBuilder().collection(collection);
|
|
1398
|
-
|
|
1399
|
-
// Add where conditions
|
|
1400
|
-
for (const [field, value] of Object.entries(where)) {
|
|
1401
|
-
queryBuilder = queryBuilder.whereField(field).equals(value);
|
|
1402
|
-
}
|
|
1403
|
-
|
|
1404
|
-
return await queryBuilder.count();
|
|
1405
|
-
} catch (error) {
|
|
1406
|
-
console.error(`countDocuments error for ${collection}:`, error);
|
|
1407
|
-
return 0;
|
|
1408
|
-
}
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
/**
|
|
1412
|
-
* Generate a unique ID for documents (simple base62 implementation)
|
|
1413
|
-
* Override this if you want to use a different ID generation strategy
|
|
1414
|
-
*
|
|
1415
|
-
* @returns Unique ID string
|
|
1416
|
-
*/
|
|
1417
|
-
generateId(): string {
|
|
1418
|
-
const chars = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
|
1419
|
-
let id = '';
|
|
1420
|
-
for (let i = 0; i < 24; i++) {
|
|
1421
|
-
id += chars[Math.floor(Math.random() * chars.length)];
|
|
1422
|
-
}
|
|
1423
|
-
return id;
|
|
1424
|
-
}
|
|
1425
|
-
|
|
1426
1016
|
/**
|
|
1427
1017
|
* Get task status by ticket ID
|
|
1428
1018
|
* @param ticketId - The ticket ID returned from async store operation
|
|
@@ -1575,13 +1165,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1575
1165
|
return await this.http.post<UploadBlobResponse>(
|
|
1576
1166
|
`/api/apps/${appId}/blobs/${request.collection}`,
|
|
1577
1167
|
formData,
|
|
1578
|
-
{
|
|
1579
|
-
headers: {
|
|
1580
|
-
'Content-Type': 'multipart/form-data',
|
|
1581
|
-
'X-App-Key': this.config.appKey,
|
|
1582
|
-
...headers
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1168
|
+
{ headers: { 'Content-Type': 'multipart/form-data', ...headers } }
|
|
1585
1169
|
);
|
|
1586
1170
|
};
|
|
1587
1171
|
|
|
@@ -1666,12 +1250,7 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1666
1250
|
// Retrieve blob via GET endpoint
|
|
1667
1251
|
const response = await this.http.get(
|
|
1668
1252
|
`/api/apps/${appId}/blobs/${request.collection}/${request.blob_id}`,
|
|
1669
|
-
{
|
|
1670
|
-
responseType: 'arraybuffer',
|
|
1671
|
-
headers: {
|
|
1672
|
-
'X-App-Key': this.config.appKey
|
|
1673
|
-
}
|
|
1674
|
-
}
|
|
1253
|
+
{ responseType: 'arraybuffer' }
|
|
1675
1254
|
);
|
|
1676
1255
|
|
|
1677
1256
|
// Return as Blob in browser, Buffer in Node.js
|
|
@@ -1689,49 +1268,6 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1689
1268
|
}
|
|
1690
1269
|
}
|
|
1691
1270
|
|
|
1692
|
-
/**
|
|
1693
|
-
* Query blob metadata using the standard query interface
|
|
1694
|
-
*
|
|
1695
|
-
* Returns metadata about blobs without downloading the actual binary data.
|
|
1696
|
-
* Useful for listing, filtering, and searching blobs by their metadata.
|
|
1697
|
-
*
|
|
1698
|
-
* @param collection - Blob collection name
|
|
1699
|
-
* @param where - Query conditions for filtering blobs
|
|
1700
|
-
* @returns Promise resolving to array of blob metadata records
|
|
1701
|
-
*
|
|
1702
|
-
* @example
|
|
1703
|
-
* ```typescript
|
|
1704
|
-
* // Query all blobs by user
|
|
1705
|
-
* const userBlobs = await client.queryBlobMetadata('avatars', {
|
|
1706
|
-
* user_address: 'celestia1abc...'
|
|
1707
|
-
* });
|
|
1708
|
-
*
|
|
1709
|
-
* // Query blobs by content type
|
|
1710
|
-
* const images = await client.queryBlobMetadata('uploads', {
|
|
1711
|
-
* content_type: { $regex: 'image/' }
|
|
1712
|
-
* });
|
|
1713
|
-
*
|
|
1714
|
-
* // Query recent blobs
|
|
1715
|
-
* const recentBlobs = await client.queryBlobMetadata('files', {
|
|
1716
|
-
* uploaded_at: { $gte: '2024-01-01T00:00:00Z' }
|
|
1717
|
-
* });
|
|
1718
|
-
*
|
|
1719
|
-
* // Access blob metadata
|
|
1720
|
-
* for (const blob of userBlobs) {
|
|
1721
|
-
* console.log('Blob ID:', blob.blob_id);
|
|
1722
|
-
* console.log('Size:', blob.size_bytes);
|
|
1723
|
-
* console.log('Type:', blob.content_type);
|
|
1724
|
-
* console.log('Custom fields:', blob);
|
|
1725
|
-
* }
|
|
1726
|
-
* ```
|
|
1727
|
-
*/
|
|
1728
|
-
async queryBlobMetadata(
|
|
1729
|
-
collection: string,
|
|
1730
|
-
where: Record<string, any> = {}
|
|
1731
|
-
): Promise<BlobMetadata[]> {
|
|
1732
|
-
return await this.findMany<BlobMetadata>(collection, where);
|
|
1733
|
-
}
|
|
1734
|
-
|
|
1735
1271
|
/**
|
|
1736
1272
|
* Get default index type based on field type
|
|
1737
1273
|
*/
|
|
@@ -1750,6 +1286,17 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1750
1286
|
}
|
|
1751
1287
|
}
|
|
1752
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
|
+
|
|
1753
1300
|
private validateStoreRequest(request: StoreRequest): void {
|
|
1754
1301
|
// Validate that either root or collection is provided
|
|
1755
1302
|
if (!request.root && !request.collection) {
|
|
@@ -1786,67 +1333,17 @@ export class OnChainDBClient extends EventEmitter<TransactionEvents> {
|
|
|
1786
1333
|
}
|
|
1787
1334
|
}
|
|
1788
1335
|
|
|
1789
|
-
private handleHttpError(error: AxiosError): OnChainDBError {
|
|
1790
|
-
console.error(error);
|
|
1791
|
-
if (error.response) {
|
|
1792
|
-
const statusCode = error.response.status;
|
|
1793
|
-
const message = (error.response.data as any)?.error || error.message;
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
if (statusCode >= 400 && statusCode < 500) {
|
|
1797
|
-
return new ValidationError(message, error.response.data);
|
|
1798
|
-
}
|
|
1799
|
-
|
|
1800
|
-
return new OnChainDBError(
|
|
1801
|
-
message,
|
|
1802
|
-
'HTTP_ERROR',
|
|
1803
|
-
statusCode,
|
|
1804
|
-
error.response.data
|
|
1805
|
-
);
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
if (error.request) {
|
|
1809
|
-
return new OnChainDBError(
|
|
1810
|
-
'Network error - could not reach OnChainDB service',
|
|
1811
|
-
'NETWORK_ERROR'
|
|
1812
|
-
);
|
|
1813
|
-
}
|
|
1814
|
-
|
|
1815
|
-
return new OnChainDBError(error.message, 'UNKNOWN_ERROR');
|
|
1816
|
-
}
|
|
1817
|
-
|
|
1818
1336
|
private sleep(ms: number): Promise<void> {
|
|
1819
1337
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1820
1338
|
}
|
|
1821
1339
|
|
|
1822
|
-
/**
|
|
1823
|
-
* Build root string from collection name using configured appId
|
|
1824
|
-
*
|
|
1825
|
-
* @param collection - Collection name
|
|
1826
|
-
* @returns Full root string in format "appId::collection" or just collection for system ops
|
|
1827
|
-
*/
|
|
1828
|
-
private buildRoot(collection: string): string {
|
|
1829
|
-
if (!this.config.appId) {
|
|
1830
|
-
return collection; // System operation or no appId configured
|
|
1831
|
-
}
|
|
1832
|
-
return `${this.config.appId}::${collection}`;
|
|
1833
|
-
}
|
|
1834
|
-
|
|
1835
|
-
/**
|
|
1836
|
-
* Resolve root parameter from request, building it if needed
|
|
1837
|
-
*
|
|
1838
|
-
* @param request - Store or Query request
|
|
1839
|
-
* @returns Resolved root string
|
|
1840
|
-
*/
|
|
1841
1340
|
private resolveRoot(request: { root?: string; collection?: string }): string {
|
|
1842
1341
|
if (request.root) {
|
|
1843
|
-
return request.root;
|
|
1342
|
+
return request.root;
|
|
1844
1343
|
}
|
|
1845
|
-
|
|
1846
1344
|
if (request.collection) {
|
|
1847
|
-
return this.
|
|
1345
|
+
return this.config.appId ? `${this.config.appId}::${request.collection}` : request.collection;
|
|
1848
1346
|
}
|
|
1849
|
-
|
|
1850
1347
|
throw new ValidationError('Either root or collection must be provided');
|
|
1851
1348
|
}
|
|
1852
1349
|
|