@moogodev/client 0.1.1 → 0.1.2

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/src/client.ts CHANGED
@@ -23,6 +23,13 @@ import type {
23
23
  CreateCredentialResponse,
24
24
  RotateCredentialResponse,
25
25
  DeleteResponse,
26
+ // SQL types
27
+ QueryRequest,
28
+ QueryResponse,
29
+ ExecRequest,
30
+ ExecResponse,
31
+ TransactionRequest,
32
+ TransactionResponse,
26
33
  } from './types.js';
27
34
 
28
35
  import {
@@ -60,25 +67,44 @@ const DEFAULT_TIMEOUT = 30_000;
60
67
  export class MoogoClient {
61
68
  private readonly baseUrl: string;
62
69
  private readonly projectId: string;
63
- private readonly accessKeyId: string;
64
- private readonly secretKey: string;
70
+ private readonly accessKeyId?: string;
71
+ private readonly secretKey?: string;
72
+ private readonly projectKey?: string;
65
73
  private readonly fetchImpl: typeof fetch;
66
74
  private readonly timeout: number;
67
75
 
68
76
  constructor(config: ClientConfig) {
69
77
  if (!config.baseUrl) throw new Error('baseUrl is required');
70
78
  if (!config.projectId) throw new Error('projectId is required');
71
- if (!config.accessKeyId) throw new Error('accessKeyId is required');
72
- if (!config.secretKey) throw new Error('secretKey is required');
79
+ if (!config.accessKeyId && !config.projectKey) {
80
+ throw new Error('Either accessKeyId/secretKey (storage) or projectKey (SQL) is required');
81
+ }
82
+ if (config.accessKeyId && !config.secretKey) {
83
+ throw new Error('secretKey is required when accessKeyId is provided');
84
+ }
85
+ if (config.secretKey && !config.accessKeyId) {
86
+ throw new Error('accessKeyId is required when secretKey is provided');
87
+ }
73
88
 
74
89
  this.baseUrl = config.baseUrl.replace(/\/+$/, '');
75
90
  this.projectId = config.projectId;
76
91
  this.accessKeyId = config.accessKeyId;
77
92
  this.secretKey = config.secretKey;
93
+ this.projectKey = config.projectKey;
78
94
  this.fetchImpl = config.fetch ?? fetch;
79
95
  this.timeout = config.timeout ?? DEFAULT_TIMEOUT;
80
96
  }
81
97
 
98
+ /** Check if configured for SQL operations (project key) */
99
+ private hasProjectKey(): boolean {
100
+ return !!this.projectKey;
101
+ }
102
+
103
+ /** Check if configured for storage operations (access key) */
104
+ private hasStorageCredentials(): boolean {
105
+ return !!this.accessKeyId && !!this.secretKey;
106
+ }
107
+
82
108
  /** Build the bucket catalog path */
83
109
  private getCatalogPath(): string {
84
110
  return `${this.baseUrl}/buckets/${this.projectId}`;
@@ -176,8 +202,8 @@ export class MoogoClient {
176
202
  .join('');
177
203
  }
178
204
 
179
- /** Execute a request with auth and error handling */
180
- private async request<T>(
205
+ /** Execute a storage request with HMAC auth and error handling */
206
+ private async requestStorage<T>(
181
207
  method: string,
182
208
  path: string,
183
209
  options: {
@@ -187,6 +213,10 @@ export class MoogoClient {
187
213
  signal?: AbortSignal;
188
214
  } = {}
189
215
  ): Promise<T> {
216
+ if (!this.hasStorageCredentials()) {
217
+ throw new Error('Storage operations require accessKeyId and secretKey');
218
+ }
219
+
190
220
  const url = new URL(path, this.baseUrl);
191
221
  if (options.query) {
192
222
  Object.entries(options.query).forEach(([k, v]) => {
@@ -195,7 +225,7 @@ export class MoogoClient {
195
225
  }
196
226
 
197
227
  const requestHeaders: Record<string, string> = {
198
- 'X-Moogo-Access-Key-Id': this.accessKeyId,
228
+ 'X-Moogo-Access-Key-Id': this.accessKeyId!,
199
229
  'Content-Type': 'application/json',
200
230
  ...options.headers,
201
231
  };
@@ -250,17 +280,87 @@ export class MoogoClient {
250
280
  }
251
281
  }
252
282
 
283
+ /** Execute a SQL request with Bearer token auth and error handling */
284
+ private async requestSQL<T>(
285
+ method: string,
286
+ path: string,
287
+ options: {
288
+ headers?: Record<string, string>;
289
+ body?: BodyInit;
290
+ query?: Record<string, string>;
291
+ signal?: AbortSignal;
292
+ } = {}
293
+ ): Promise<T> {
294
+ if (!this.hasProjectKey()) {
295
+ throw new Error('SQL operations require projectKey');
296
+ }
297
+
298
+ const url = new URL(path, this.baseUrl);
299
+ if (options.query) {
300
+ Object.entries(options.query).forEach(([k, v]) => {
301
+ if (v !== undefined && v !== null) url.searchParams.set(k, v);
302
+ });
303
+ }
304
+
305
+ const requestHeaders: Record<string, string> = {
306
+ 'Authorization': `Bearer ${this.projectKey}`,
307
+ 'Content-Type': 'application/json',
308
+ ...options.headers,
309
+ };
310
+
311
+ const controller = new AbortController();
312
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
313
+
314
+ const signal = options.signal
315
+ ? AbortSignal.any([options.signal, controller.signal])
316
+ : controller.signal;
317
+
318
+ try {
319
+ const response = await this.fetchImpl(url.toString(), {
320
+ method,
321
+ headers: requestHeaders,
322
+ body: options.body,
323
+ signal,
324
+ });
325
+
326
+ clearTimeout(timeoutId);
327
+
328
+ const contentType = response.headers.get('Content-Type') || '';
329
+ const isJson = contentType.includes('application/json');
330
+
331
+ let data: any;
332
+ if (isJson) {
333
+ data = await response.json();
334
+ } else {
335
+ data = await response.text();
336
+ }
337
+
338
+ if (!response.ok) {
339
+ throw createErrorFromResponse(response, data);
340
+ }
341
+
342
+ return data as T;
343
+ } catch (error) {
344
+ clearTimeout(timeoutId);
345
+ if (error instanceof MoogoError) throw error;
346
+ if (error instanceof DOMException && error.name === 'AbortError') {
347
+ throw new MoogoError('timeout', 'Request timed out', 408);
348
+ }
349
+ throw new MoogoError('network_error', `Network error: ${error}`, 0);
350
+ }
351
+ }
352
+
253
353
  // ==================== Bucket Operations ====================
254
354
 
255
355
  /** List all buckets in the project */
256
356
  async listBuckets(): Promise<Bucket[]> {
257
- const response = await this.request<ListBucketsResponse>('GET', this.getCatalogPath());
357
+ const response = await this.requestStorage<ListBucketsResponse>('GET', this.getCatalogPath());
258
358
  return response.buckets;
259
359
  }
260
360
 
261
361
  /** Get bucket info including usage stats */
262
362
  async getBucketInfo(bucketId: string): Promise<BucketInfoResponse> {
263
- const response = await this.request<BucketInfoResponse>(
363
+ const response = await this.requestStorage<BucketInfoResponse>(
264
364
  'GET',
265
365
  `${this.getCatalogPath()}/${bucketId}`
266
366
  );
@@ -269,7 +369,7 @@ export class MoogoClient {
269
369
 
270
370
  /** Create a new bucket */
271
371
  async createBucket(request: CreateBucketRequest): Promise<Bucket> {
272
- const response = await this.request<{ success: true; bucket: Bucket }>(
372
+ const response = await this.requestStorage<{ success: true; bucket: Bucket }>(
273
373
  'POST',
274
374
  this.getCatalogPath(),
275
375
  { body: JSON.stringify(request) }
@@ -279,7 +379,7 @@ export class MoogoClient {
279
379
 
280
380
  /** Update bucket settings */
281
381
  async updateBucket(bucketId: string, request: UpdateBucketRequest): Promise<Bucket> {
282
- const response = await this.request<{ success: true; bucket: Bucket }>(
382
+ const response = await this.requestStorage<{ success: true; bucket: Bucket }>(
283
383
  'PATCH',
284
384
  `${this.getCatalogPath()}/${bucketId}`,
285
385
  { body: JSON.stringify(request) }
@@ -289,12 +389,12 @@ export class MoogoClient {
289
389
 
290
390
  /** Delete a bucket and all its objects */
291
391
  async deleteBucket(bucketId: string): Promise<void> {
292
- await this.request<DeleteResponse>('DELETE', `${this.getCatalogPath()}/${bucketId}`);
392
+ await this.requestStorage<DeleteResponse>('DELETE', `${this.getCatalogPath()}/${bucketId}`);
293
393
  }
294
394
 
295
395
  /** Toggle bucket public/private (fans out to all objects) */
296
396
  async setBucketPublic(bucketId: string, isPublic: boolean): Promise<Bucket> {
297
- const response = await this.request<{ success: true; bucket: Bucket }>(
397
+ const response = await this.requestStorage<{ success: true; bucket: Bucket }>(
298
398
  'POST',
299
399
  `${this.getCatalogPath()}/${bucketId}/public`,
300
400
  { body: JSON.stringify({ is_public: isPublic }) }
@@ -328,6 +428,10 @@ export class MoogoClient {
328
428
  const query: Record<string, string> = {};
329
429
  if (bucket !== 'default') query.bucket = bucket;
330
430
 
431
+ if (!this.hasStorageCredentials()) {
432
+ throw new Error('Upload requires storage credentials (accessKeyId and secretKey)');
433
+ }
434
+
331
435
  const requestHeaders: Record<string, string> = {};
332
436
  if (contentType) requestHeaders['Content-Type'] = contentType;
333
437
  if (cacheControl) requestHeaders['Cache-Control'] = cacheControl;
@@ -355,7 +459,7 @@ export class MoogoClient {
355
459
  Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
356
460
 
357
461
  const authHeaders: Record<string, string> = {
358
- 'X-Moogo-Access-Key-Id': this.accessKeyId,
462
+ 'X-Moogo-Access-Key-Id': this.accessKeyId!,
359
463
  ...requestHeaders,
360
464
  };
361
465
 
@@ -436,8 +540,12 @@ export class MoogoClient {
436
540
  const query: Record<string, string> = {};
437
541
  if (bucket !== 'default') query.bucket = bucket;
438
542
 
543
+ if (!this.hasStorageCredentials()) {
544
+ throw new Error('Download requires storage credentials (accessKeyId and secretKey)');
545
+ }
546
+
439
547
  const requestHeaders: Record<string, string> = {
440
- 'X-Moogo-Access-Key-Id': this.accessKeyId,
548
+ 'X-Moogo-Access-Key-Id': this.accessKeyId!,
441
549
  };
442
550
  if (range) {
443
551
  const end = range.end !== undefined ? range.end : '';
@@ -508,11 +616,15 @@ export class MoogoClient {
508
616
  const query: Record<string, string> = {};
509
617
  if (bucket !== 'default') query.bucket = bucket;
510
618
 
619
+ if (!this.hasStorageCredentials()) {
620
+ throw new Error('HEAD requires storage credentials (accessKeyId and secretKey)');
621
+ }
622
+
511
623
  const url = new URL(`${this.baseUrl}${path}`);
512
624
  Object.entries(query).forEach(([k, v]) => url.searchParams.set(k, v));
513
625
 
514
626
  const requestHeaders: Record<string, string> = {
515
- 'X-Moogo-Access-Key-Id': this.accessKeyId,
627
+ 'X-Moogo-Access-Key-Id': this.accessKeyId!,
516
628
  };
517
629
 
518
630
  const authHeader = await this.createAuthHeader('HEAD', url.pathname + url.search, requestHeaders);
@@ -559,7 +671,7 @@ export class MoogoClient {
559
671
  const query: Record<string, string> = {};
560
672
  if (bucket !== 'default') query.bucket = bucket;
561
673
 
562
- await this.request('DELETE', path, { query, signal });
674
+ await this.requestStorage('DELETE', path, { query, signal });
563
675
  }
564
676
 
565
677
  /** List objects in a bucket */
@@ -573,7 +685,7 @@ export class MoogoClient {
573
685
  if (query.limit) params.limit = String(query.limit);
574
686
  if (query.cursor) params.cursor = query.cursor;
575
687
 
576
- return this.request<ListObjectsResponse>('GET', `/p/${this.projectId}/bucket`, { query: params });
688
+ return this.requestStorage<ListObjectsResponse>('GET', `/p/${this.projectId}/bucket`, { query: params });
577
689
  }
578
690
 
579
691
  /** Get a presigned URL for an object */
@@ -588,7 +700,7 @@ export class MoogoClient {
588
700
  const query: Record<string, string> = {};
589
701
  if (bucket !== 'default') query.bucket = bucket;
590
702
 
591
- return this.request<PresignedUrlResponse>('POST', path, {
703
+ return this.requestStorage<PresignedUrlResponse>('POST', path, {
592
704
  query,
593
705
  body: JSON.stringify(request),
594
706
  signal,
@@ -607,7 +719,7 @@ export class MoogoClient {
607
719
  const query: Record<string, string> = {};
608
720
  if (bucket !== 'default') query.bucket = bucket;
609
721
 
610
- return this.request<SignedUploadResponse>('POST', path, {
722
+ return this.requestStorage<SignedUploadResponse>('POST', path, {
611
723
  query,
612
724
  body: JSON.stringify(request),
613
725
  signal,
@@ -626,7 +738,7 @@ export class MoogoClient {
626
738
  const query: Record<string, string> = {};
627
739
  if (bucket !== 'default') query.bucket = bucket;
628
740
 
629
- const response = await this.request<{ success: true; object: ObjectResponse }>(
741
+ const response = await this.requestStorage<{ success: true; object: ObjectResponse }>(
630
742
  'PATCH',
631
743
  path,
632
744
  { query, body: JSON.stringify(updates), signal }
@@ -638,7 +750,7 @@ export class MoogoClient {
638
750
 
639
751
  /** List storage credentials */
640
752
  async listCredentials(): Promise<StorageCredential[]> {
641
- const response = await this.request<ListCredentialsResponse>(
753
+ const response = await this.requestStorage<ListCredentialsResponse>(
642
754
  'GET',
643
755
  this.getCredentialsPath()
644
756
  );
@@ -647,7 +759,7 @@ export class MoogoClient {
647
759
 
648
760
  /** Create a new storage credential */
649
761
  async createCredential(request: CreateCredentialRequest): Promise<CreateCredentialResponse> {
650
- return this.request<CreateCredentialResponse>(
762
+ return this.requestStorage<CreateCredentialResponse>(
651
763
  'POST',
652
764
  this.getCredentialsPath(),
653
765
  { body: JSON.stringify(request) }
@@ -656,7 +768,7 @@ export class MoogoClient {
656
768
 
657
769
  /** Rotate a credential (generates new secret) */
658
770
  async rotateCredential(credentialId: string): Promise<RotateCredentialResponse> {
659
- return this.request<RotateCredentialResponse>(
771
+ return this.requestStorage<RotateCredentialResponse>(
660
772
  'POST',
661
773
  `${this.getCredentialsPath()}/${credentialId}/rotate`
662
774
  );
@@ -664,7 +776,7 @@ export class MoogoClient {
664
776
 
665
777
  /** Revoke a credential */
666
778
  async revokeCredential(credentialId: string): Promise<void> {
667
- await this.request<DeleteResponse>(
779
+ await this.requestStorage<DeleteResponse>(
668
780
  'DELETE',
669
781
  `${this.getCredentialsPath()}/${credentialId}`
670
782
  );
@@ -706,11 +818,54 @@ export class MoogoClient {
706
818
  if (!result.exists) return null;
707
819
  return result.headers.get('ETag');
708
820
  }
821
+
822
+ // ==================== SQL Operations ====================
823
+
824
+ /** Execute a SQL read query */
825
+ async query(request: QueryRequest, options: { signal?: AbortSignal } = {}): Promise<QueryResponse> {
826
+ return this.requestSQL<QueryResponse>('POST', `/p/${this.projectId}/query`, {
827
+ body: JSON.stringify(request),
828
+ signal: options.signal,
829
+ });
830
+ }
831
+
832
+ /** Execute a SQL write statement */
833
+ async exec(request: ExecRequest, options: { signal?: AbortSignal } = {}): Promise<ExecResponse> {
834
+ return this.requestSQL<ExecResponse>('POST', `/p/${this.projectId}/exec`, {
835
+ body: JSON.stringify(request),
836
+ signal: options.signal,
837
+ });
838
+ }
839
+
840
+ /** Execute multiple SQL statements in a transaction */
841
+ async transaction(request: TransactionRequest, options: { signal?: AbortSignal } = {}): Promise<TransactionResponse> {
842
+ return this.requestSQL<TransactionResponse>('POST', `/p/${this.projectId}/transaction`, {
843
+ body: JSON.stringify(request),
844
+ signal: options.signal,
845
+ });
846
+ }
847
+
848
+ /** Run database maintenance (VACUUM, REINDEX, ANALYZE) */
849
+ async maintenance(options: { signal?: AbortSignal } = {}): Promise<{ success: true; reclaimed_bytes: number }> {
850
+ return this.requestSQL<{ success: true; reclaimed_bytes: number }>(
851
+ 'POST',
852
+ `/p/${this.projectId}/maintenance`,
853
+ { signal: options.signal }
854
+ );
855
+ }
709
856
  }
710
857
 
711
858
  /** Convenience function to create a client from environment variables */
712
859
  export function createClientFromEnv(env?: Record<string, string | undefined>): MoogoClient {
713
860
  const getEnv = (key: string) => env?.[key] ?? process.env[key];
861
+ const projectKey = getEnv('MOOGO_PROJECT_KEY');
862
+ if (projectKey) {
863
+ return new MoogoClient({
864
+ baseUrl: getEnv('MOOGO_API_URL') || 'https://api.moogo.dev',
865
+ projectId: getEnv('MOOGO_PROJECT_ID')!,
866
+ projectKey,
867
+ });
868
+ }
714
869
  return new MoogoClient({
715
870
  baseUrl: getEnv('MOOGO_API_URL') || 'https://api.moogo.dev',
716
871
  projectId: getEnv('MOOGO_PROJECT_ID')!,
package/src/index.ts CHANGED
@@ -58,6 +58,15 @@ export type {
58
58
  CreateCredentialResponse,
59
59
  RotateCredentialResponse,
60
60
  DeleteResponse,
61
+ // SQL types
62
+ QueryRequest,
63
+ QueryRow,
64
+ QueryResponse,
65
+ ExecRequest,
66
+ ExecResponse,
67
+ TransactionRequest,
68
+ TransactionResponse,
69
+ TransactionResult,
61
70
  } from './types.js';
62
71
 
63
72
  // Errors
package/src/types.ts CHANGED
@@ -187,10 +187,12 @@ export interface ClientConfig {
187
187
  baseUrl: string;
188
188
  /** Project ID */
189
189
  projectId: string;
190
- /** Access Key ID */
191
- accessKeyId: string;
192
- /** Secret Key */
193
- secretKey: string;
190
+ /** Access Key ID (for storage operations) */
191
+ accessKeyId?: string;
192
+ /** Secret Key (for storage operations) */
193
+ secretKey?: string;
194
+ /** Project Key (for SQL operations) - used instead of accessKeyId/secretKey */
195
+ projectKey?: string;
194
196
  /** Optional: custom fetch implementation */
195
197
  fetch?: typeof fetch;
196
198
  /** Optional: default timeout in ms */
@@ -203,4 +205,59 @@ export interface ParsedAuth {
203
205
  signature: string;
204
206
  timestamp: string;
205
207
  signedHeaders: string;
208
+ }
209
+
210
+ /** SQL query request */
211
+ export interface QueryRequest {
212
+ sql: string;
213
+ args?: any[];
214
+ }
215
+
216
+ /** SQL query response row */
217
+ export interface QueryRow {
218
+ [column: string]: any;
219
+ }
220
+
221
+ /** SQL query response */
222
+ export interface QueryResponse {
223
+ success: true;
224
+ rows: QueryRow[];
225
+ row_count: number;
226
+ columns: string[];
227
+ truncated: boolean;
228
+ duration_ms: number;
229
+ }
230
+
231
+ /** SQL exec request */
232
+ export interface ExecRequest {
233
+ sql: string;
234
+ args?: any[];
235
+ }
236
+
237
+ /** SQL exec response */
238
+ export interface ExecResponse {
239
+ success: true;
240
+ rows_affected: number;
241
+ duration_ms: number;
242
+ last_insert_id?: number;
243
+ }
244
+
245
+ /** SQL transaction request */
246
+ export interface TransactionRequest {
247
+ statements: (string | QueryRequest)[];
248
+ }
249
+
250
+ /** SQL transaction response */
251
+ export interface TransactionResponse {
252
+ success: true;
253
+ results: TransactionResult[];
254
+ duration_ms: number;
255
+ }
256
+
257
+ export interface TransactionResult {
258
+ rows?: QueryRow[];
259
+ row_count?: number;
260
+ rows_affected?: number;
261
+ columns?: string[];
262
+ truncated?: boolean;
206
263
  }