@scarletdb/sdk 0.1.2 → 0.2.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/dist/index.mjs CHANGED
@@ -77,19 +77,23 @@ var HttpClient = class {
77
77
  url += `?${queryString}`;
78
78
  }
79
79
  }
80
+ const authHeaders = await this.config.getAuthHeaders({ method, url });
80
81
  const requestHeaders = {
81
- "Authorization": `Bearer ${this.config.apiKey}`,
82
- "Content-Type": "application/json",
83
- "Accept": "application/json",
82
+ Accept: "application/json",
83
+ ...authHeaders,
84
84
  ...headers
85
85
  };
86
+ const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
87
+ if (body !== void 0 && !isFormData) {
88
+ requestHeaders["Content-Type"] = requestHeaders["Content-Type"] || "application/json";
89
+ }
86
90
  const controller = new AbortController();
87
91
  const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
88
92
  try {
89
93
  const response = await fetch(url, {
90
94
  method,
91
95
  headers: requestHeaders,
92
- body: body ? JSON.stringify(body) : void 0,
96
+ body: body ? isFormData ? body : JSON.stringify(body) : void 0,
93
97
  signal: controller.signal
94
98
  });
95
99
  clearTimeout(timeoutId);
@@ -137,54 +141,29 @@ var HttpClient = class {
137
141
  }
138
142
  }
139
143
  // Convenience methods
140
- async get(path, params) {
141
- const response = await this.request({ method: "GET", path, params });
144
+ async get(path, params, headers) {
145
+ const response = await this.request({ method: "GET", path, params, headers });
142
146
  return response.data;
143
147
  }
144
- async post(path, body) {
145
- const response = await this.request({ method: "POST", path, body });
148
+ async post(path, body, headers) {
149
+ const response = await this.request({ method: "POST", path, body, headers });
146
150
  return response.data;
147
151
  }
148
- async patch(path, body) {
149
- const response = await this.request({ method: "PATCH", path, body });
152
+ async patch(path, body, headers) {
153
+ const response = await this.request({ method: "PATCH", path, body, headers });
150
154
  return response.data;
151
155
  }
152
- async put(path, body) {
153
- const response = await this.request({ method: "PUT", path, body });
156
+ async put(path, body, headers) {
157
+ const response = await this.request({ method: "PUT", path, body, headers });
154
158
  return response.data;
155
159
  }
156
- async delete(path, body) {
157
- const response = await this.request({ method: "DELETE", path, body });
160
+ async delete(path, body, headers) {
161
+ const response = await this.request({ method: "DELETE", path, body, headers });
158
162
  return response.data;
159
163
  }
160
- /**
161
- * Upload file using multipart/form-data
162
- */
163
- async upload(path, file, fileName) {
164
- const formData = new FormData();
165
- formData.append("file", file, fileName);
166
- const controller = new AbortController();
167
- const timeoutId = setTimeout(() => controller.abort(), this.config.timeout * 2);
168
- try {
169
- const response = await fetch(`${this.config.baseUrl}${path}`, {
170
- method: "POST",
171
- headers: {
172
- "Authorization": `Bearer ${this.config.apiKey}`
173
- },
174
- body: formData,
175
- signal: controller.signal
176
- });
177
- clearTimeout(timeoutId);
178
- const data = await response.json();
179
- if (!response.ok) {
180
- this.handleErrorResponse(response.status, data);
181
- }
182
- return data;
183
- } catch (error) {
184
- clearTimeout(timeoutId);
185
- if (error instanceof ScarletError) throw error;
186
- throw new NetworkError("Upload failed", error);
187
- }
164
+ async postForm(path, form) {
165
+ const response = await this.request({ method: "POST", path, body: form });
166
+ return response.data;
188
167
  }
189
168
  };
190
169
 
@@ -205,37 +184,41 @@ var QueryBuilder = class {
205
184
  this.client = client;
206
185
  this.table = table;
207
186
  }
208
- _columns = [];
209
- _where = {};
210
- _orderBy;
187
+ _fields = [];
188
+ _filters = {};
189
+ _sort;
190
+ _page;
211
191
  _limit;
212
- _offset;
213
192
  /**
214
193
  * Select specific columns (default: all)
215
194
  */
216
195
  select(...columns) {
217
- this._columns = columns;
196
+ this._fields = columns;
218
197
  return this;
219
198
  }
220
199
  /**
221
200
  * Add where conditions
222
201
  */
223
202
  where(conditions) {
224
- this._where = { ...this._where, ...conditions };
203
+ this._filters = { ...this._filters, ...conditions };
225
204
  return this;
226
205
  }
227
206
  /**
228
207
  * Add equality condition
229
208
  */
230
209
  eq(column, value) {
231
- this._where[column] = value;
210
+ this._filters[column] = value;
232
211
  return this;
233
212
  }
234
213
  /**
235
214
  * Order results
236
215
  */
237
216
  orderBy(column, direction = "asc") {
238
- this._orderBy = { column, direction };
217
+ this._sort = { column, direction };
218
+ return this;
219
+ }
220
+ page(page) {
221
+ this._page = page;
239
222
  return this;
240
223
  }
241
224
  /**
@@ -245,31 +228,47 @@ var QueryBuilder = class {
245
228
  this._limit = count;
246
229
  return this;
247
230
  }
248
- /**
249
- * Offset results (for pagination)
250
- */
251
- offset(count) {
252
- this._offset = count;
253
- return this;
231
+ buildQueryParams() {
232
+ const params = {};
233
+ const page = this._page ?? 1;
234
+ const limit = this._limit ?? void 0;
235
+ params.page = page;
236
+ if (limit !== void 0) params.limit = limit;
237
+ if (this._sort) {
238
+ params.sort = this._sort.direction === "desc" ? `-${this._sort.column}` : this._sort.column;
239
+ }
240
+ if (this._fields.length > 0) {
241
+ params.fields = this._fields.join(",");
242
+ }
243
+ for (const [field, value] of Object.entries(this._filters)) {
244
+ if (value === void 0) continue;
245
+ if (value && typeof value === "object" && !Array.isArray(value)) {
246
+ for (const [op, opValue] of Object.entries(value)) {
247
+ params[`filter[${field}][${op}]`] = String(opValue);
248
+ }
249
+ } else {
250
+ params[`filter[${field}]`] = String(value);
251
+ }
252
+ }
253
+ return params;
254
254
  }
255
255
  /**
256
256
  * Execute SELECT query
257
257
  */
258
258
  async execute() {
259
- const params = {};
260
- if (this._limit !== void 0) params.limit = this._limit;
261
- if (this._offset !== void 0) params.offset = this._offset;
262
- if (this._orderBy) {
263
- params.sortBy = this._orderBy.column;
264
- params.sortDir = this._orderBy.direction;
265
- }
266
- if (Object.keys(this._where).length > 0) {
267
- params.search = JSON.stringify(this._where);
268
- }
269
- return this.client.get(
270
- `/tables/${this.table}/rows`,
259
+ const params = this.buildQueryParams();
260
+ const res = await this.client.get(
261
+ `/${this.table}`,
271
262
  params
272
263
  );
264
+ const offset = ((res.meta.page || 1) - 1) * (res.meta.limit || 0);
265
+ return {
266
+ rows: res.data,
267
+ total: res.meta.total,
268
+ limit: res.meta.limit,
269
+ offset,
270
+ hasMore: res.meta.page < res.meta.totalPages
271
+ };
273
272
  }
274
273
  /**
275
274
  * Execute and return all rows
@@ -284,6 +283,7 @@ var QueryBuilder = class {
284
283
  */
285
284
  async single() {
286
285
  this._limit = 1;
286
+ this._page = 1;
287
287
  const result = await this.execute();
288
288
  return result.rows[0] ?? null;
289
289
  }
@@ -291,49 +291,51 @@ var QueryBuilder = class {
291
291
  * Insert data
292
292
  */
293
293
  async insert(data) {
294
- return this.client.post(
295
- `/tables/${this.table}/rows`,
296
- Array.isArray(data) ? { rows: data } : data
297
- );
294
+ if (Array.isArray(data)) {
295
+ const created2 = await Promise.all(data.map((row) => this.client.post(`/${this.table}`, row)));
296
+ return { success: true, rows: created2.map((r) => r.data) };
297
+ }
298
+ const created = await this.client.post(`/${this.table}`, data);
299
+ return { success: true, row: created.data };
298
300
  }
299
301
  /**
300
302
  * Update data (requires where clause)
301
303
  */
302
304
  async update(data) {
303
- if (Object.keys(this._where).length === 0) {
304
- throw new Error("Update requires at least one where condition for safety");
305
+ const id = this._filters["id"];
306
+ if (!id) {
307
+ throw new Error("Update requires an id filter");
305
308
  }
306
- return this.client.patch(
307
- `/tables/${this.table}/rows`,
308
- { filters: this._where, data }
309
+ await this.client.patch(
310
+ `/${this.table}/${encodeURIComponent(String(id))}`,
311
+ data
309
312
  );
313
+ return { success: true, count: 1 };
310
314
  }
311
315
  /**
312
316
  * Delete data (requires where clause)
313
317
  */
314
318
  async delete() {
315
- if (Object.keys(this._where).length === 0) {
316
- throw new Error("Delete requires at least one where condition for safety");
319
+ const id = this._filters["id"];
320
+ if (!id) {
321
+ throw new Error("Delete requires an id filter");
317
322
  }
318
- return this.client.delete(
319
- `/tables/${this.table}/rows`,
320
- { filters: this._where }
321
- );
323
+ await this.client.delete(`/${this.table}/${encodeURIComponent(String(id))}`);
324
+ return { success: true, count: 1 };
322
325
  }
323
326
  };
324
327
 
325
328
  // src/modules/storage.ts
326
329
  var StorageModule = class {
327
- constructor(client, baseUrl) {
330
+ constructor(client, projectId) {
328
331
  this.client = client;
329
- this.baseUrl = baseUrl;
332
+ this.projectId = projectId;
330
333
  }
331
- baseUrl;
332
334
  /**
333
335
  * Create a new storage bucket
334
336
  */
335
337
  async createBucket(name, options) {
336
- return this.client.post("/storage/buckets", {
338
+ return this.client.post(`/storage/${this.projectId}/buckets`, {
337
339
  name,
338
340
  publicAccess: options?.publicAccess ?? false
339
341
  });
@@ -342,17 +344,20 @@ var StorageModule = class {
342
344
  * List all buckets
343
345
  */
344
346
  async listBuckets() {
345
- return this.client.get("/storage/buckets");
347
+ return this.client.get(`/storage/${this.projectId}/buckets`);
346
348
  }
347
349
  /**
348
350
  * Upload a file to a bucket
349
351
  */
350
352
  async upload(bucket, file, options) {
353
+ const form = new FormData();
351
354
  const fileName = options?.fileName || (file instanceof File ? file.name : "file");
352
- return this.client.upload(
353
- `/storage/buckets/${bucket}/upload`,
354
- file,
355
- fileName
355
+ form.append("file", file, fileName);
356
+ if (options?.randomId !== void 0) form.append("randomId", String(options.randomId));
357
+ if (options?.cacheDuration !== void 0) form.append("cacheDuration", String(options.cacheDuration));
358
+ return this.client.postForm(
359
+ `/storage/${this.projectId}/buckets/${bucket}/upload`,
360
+ form
356
361
  );
357
362
  }
358
363
  /**
@@ -363,62 +368,63 @@ var StorageModule = class {
363
368
  if (options?.limit) params.limit = options.limit;
364
369
  if (options?.offset) params.offset = options.offset;
365
370
  if (options?.prefix) params.prefix = options.prefix;
366
- return this.client.get(`/storage/buckets/${bucket}/files`, params);
371
+ return this.client.get(`/storage/${this.projectId}/buckets/${bucket}/files`, params);
367
372
  }
368
373
  /**
369
374
  * Get a signed URL for private file access
370
375
  */
371
376
  async getSignedUrl(bucket, fileName) {
372
377
  const result = await this.client.get(
373
- `/storage/buckets/${bucket}/files/${encodeURIComponent(fileName)}/url`
378
+ `/storage/${this.projectId}/buckets/${bucket}/files/${encodeURIComponent(fileName)}/url`
374
379
  );
375
380
  return result.url;
376
381
  }
377
382
  /**
378
383
  * Get public URL for a file (bucket must be public)
379
384
  */
380
- getPublicUrl(bucket, fileName) {
381
- return `${this.baseUrl}/storage/buckets/${bucket}/files/${encodeURIComponent(fileName)}`;
382
- }
383
385
  /**
384
386
  * Delete a file
385
387
  */
386
388
  async delete(bucket, fileName) {
387
389
  return this.client.delete(
388
- `/storage/buckets/${bucket}/files/${encodeURIComponent(fileName)}`
390
+ `/storage/${this.projectId}/buckets/${bucket}/files/${encodeURIComponent(fileName)}`
389
391
  );
390
392
  }
391
393
  /**
392
394
  * Get storage statistics
393
395
  */
394
396
  async getStats() {
395
- return this.client.get("/storage/stats");
397
+ return this.client.get(`/storage/${this.projectId}/stats`);
396
398
  }
397
399
  };
398
400
 
399
401
  // src/modules/email.ts
400
402
  var EmailModule = class {
401
- constructor(client) {
403
+ constructor(client, projectId) {
402
404
  this.client = client;
405
+ this.projectId = projectId;
406
+ }
407
+ headers() {
408
+ return { "x-project-id": this.projectId };
403
409
  }
404
410
  /**
405
411
  * Send a transactional email
406
412
  */
407
413
  async send(options) {
408
- return this.client.post("/email/send", options);
414
+ return this.client.post("/email/send", options, this.headers());
409
415
  }
410
416
  /**
411
417
  * Add a custom email domain
412
418
  */
413
419
  async addDomain(domain) {
414
- const result = await this.client.post("/email/domains", { domain });
420
+ const result = await this.client.post("/email/domains", { domain }, this.headers());
415
421
  return result.domain;
416
422
  }
417
423
  /**
418
424
  * List configured email domains
419
425
  */
420
426
  async listDomains() {
421
- const result = await this.client.get("/email/domains");
427
+ const result = await this.client.get("/email/domains", void 0, this.headers());
422
428
  return result.domains;
423
429
  }
424
430
  /**
@@ -426,7 +432,9 @@ var EmailModule = class {
426
432
  */
427
433
  async verifyDomain(domainId) {
428
434
  return this.client.post(
429
- `/email/domains/${domainId}/verify`
435
+ `/email/domains/${domainId}/verify`,
436
+ {},
437
+ this.headers()
430
438
  );
431
439
  }
432
440
  /**
@@ -434,15 +442,16 @@ var EmailModule = class {
434
442
  */
435
443
  async getLogs(options) {
436
444
  const params = options?.limit ? { limit: options.limit } : void 0;
437
- const result = await this.client.get("/email/logs", params);
445
+ const result = await this.client.get("/email/logs", params, this.headers());
438
446
  return result.logs;
439
447
  }
440
448
  };
441
449
 
442
450
  // src/modules/ai.ts
443
451
  var AIModule = class {
444
- constructor(client) {
452
+ constructor(client, projectId) {
445
453
  this.client = client;
454
+ this.projectId = projectId;
446
455
  }
447
456
  /**
448
457
  * Execute a natural language query
@@ -454,61 +463,72 @@ var AIModule = class {
454
463
  * console.log(result.rows); // Query results
455
464
  */
456
465
  async query(prompt) {
457
- return this.client.post("/ai/query", { prompt });
466
+ return this.client.post(`/ai/${this.projectId}/query`, { prompt });
458
467
  }
459
- /**
460
- * Generate SQL from natural language without executing
461
- */
462
468
  async generateSQL(prompt) {
463
- return this.client.post("/ai/generate", { prompt });
469
+ const result = await this.query(prompt);
470
+ return { sql: result.sql, explanation: result.explanation };
464
471
  }
465
472
  };
466
473
 
467
- // src/modules/sql.ts
468
- var SQLModule = class {
469
- constructor(client) {
470
- this.client = client;
471
- }
472
- /**
473
- * Execute a parameterized SQL query
474
- *
475
- * @example
476
- * const result = await db.sql.query(
477
- * 'SELECT * FROM users WHERE created_at > $1',
478
- * [new Date('2024-01-01')]
479
- * );
480
- */
481
- async query(sql, params = []) {
482
- return this.client.post("/query", { query: sql, params });
483
- }
484
- /**
485
- * Execute raw SQL without parameter binding
486
- * ⚠️ Use with caution - ensure input is sanitized
487
- */
488
- async unsafe(sql) {
489
- return this.client.post("/query", { query: sql, params: [] });
490
- }
491
- };
492
- function createSqlTag(client) {
493
- return async function sql(strings, ...values) {
494
- let query = "";
495
- const params = [];
496
- strings.forEach((str, i) => {
497
- query += str;
498
- if (i < values.length) {
499
- params.push(values[i]);
500
- query += `$${i + 1}`;
501
- }
502
- });
503
- return client.post("/query", { query, params });
474
+ // src/dpop.ts
475
+ function base64urlEncode(data) {
476
+ let str = "";
477
+ for (let i = 0; i < data.length; i++) str += String.fromCharCode(data[i]);
478
+ const b64 = typeof btoa !== "undefined" ? btoa(str) : Buffer.from(data).toString("base64");
479
+ return b64.replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
480
+ }
481
+ function base64urlEncodeJson(value) {
482
+ const json = JSON.stringify(value);
483
+ const bytes = new TextEncoder().encode(json);
484
+ return base64urlEncode(bytes);
485
+ }
486
+ function randomId() {
487
+ const bytes = new Uint8Array(16);
488
+ crypto.getRandomValues(bytes);
489
+ return Array.from(bytes).map((b) => b.toString(16).padStart(2, "0")).join("");
490
+ }
491
+ async function createDpopKey() {
492
+ const keyPair = await crypto.subtle.generateKey(
493
+ { name: "ECDSA", namedCurve: "P-256" },
494
+ true,
495
+ ["sign", "verify"]
496
+ );
497
+ const publicJwk = await crypto.subtle.exportKey("jwk", keyPair.publicKey);
498
+ return { keyPair, publicJwk };
499
+ }
500
+ async function createDpopProof(params) {
501
+ const header = {
502
+ typ: "dpop+jwt",
503
+ alg: "ES256",
504
+ jwk: await crypto.subtle.exportKey("jwk", params.keyPair.publicKey)
505
+ };
506
+ const now = Math.floor(Date.now() / 1e3);
507
+ const payload = {
508
+ htm: params.method.toUpperCase(),
509
+ htu: new URL(params.url).toString(),
510
+ iat: now,
511
+ jti: randomId(),
512
+ iss: "scarlet-dpop",
513
+ aud: "scarlet-sdk"
504
514
  };
515
+ const encodedHeader = base64urlEncodeJson(header);
516
+ const encodedPayload = base64urlEncodeJson(payload);
517
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
518
+ const signature = await crypto.subtle.sign(
519
+ { name: "ECDSA", hash: "SHA-256" },
520
+ params.keyPair.privateKey,
521
+ new TextEncoder().encode(signingInput)
522
+ );
523
+ return `${signingInput}.${base64urlEncode(new Uint8Array(signature))}`;
505
524
  }
506
525
 
507
526
  // src/index.ts
508
- var DEFAULT_BASE_URL = "https://api.scarletdb.space/api/v1";
527
+ var DEFAULT_URL = "https://api.scarletdb.space";
509
528
  var DEFAULT_TIMEOUT = 3e4;
510
529
  var Scarlet = class {
511
- client;
530
+ engineClient;
531
+ coreClient;
512
532
  _data;
513
533
  /** Storage module for file operations */
514
534
  storage;
@@ -516,26 +536,68 @@ var Scarlet = class {
516
536
  email;
517
537
  /** AI module for natural language queries */
518
538
  ai;
519
- /** SQL module for raw queries */
520
- sql;
539
+ projectId;
521
540
  constructor(config) {
522
- if (!config.apiKey) {
523
- throw new Error("API key is required");
524
- }
525
- const baseUrl = config.baseUrl || DEFAULT_BASE_URL;
541
+ if (!config.projectId) throw new Error("projectId is required");
542
+ this.projectId = config.projectId;
543
+ const url = config.url || DEFAULT_URL;
544
+ const coreBaseUrl = `${(config.coreUrl || url).replace(/\/$/, "")}/api/v1`;
545
+ const engineBaseUrl = `${(config.engineUrl || url).replace(/\/$/, "")}/api/db/${this.projectId}`;
526
546
  const timeout = config.timeout || DEFAULT_TIMEOUT;
527
- this.client = new HttpClient({
528
- apiKey: config.apiKey,
529
- baseUrl,
530
- timeout
531
- });
532
- this._data = new DataModule(this.client);
533
- this.storage = new StorageModule(this.client, baseUrl);
534
- this.email = new EmailModule(this.client);
535
- this.ai = new AIModule(this.client);
536
- const sqlModule = new SQLModule(this.client);
537
- const sqlTag = createSqlTag(this.client);
538
- this.sql = Object.assign(sqlTag, sqlModule);
547
+ const tokenState = { value: "", expiresAt: 0 };
548
+ let dpopKey = null;
549
+ const getToken = async () => {
550
+ if (config.token) return config.token;
551
+ if (config.getToken) return config.getToken();
552
+ if (config.tokenEndpoint || config.publishableKey || !config.apiKey) {
553
+ const endpoint = config.tokenEndpoint || (config.publishableKey ? "/auth/publishable-token" : "/auth/sdk-token");
554
+ const now = Date.now();
555
+ if (tokenState.value && tokenState.expiresAt > now + 1e4) {
556
+ return tokenState.value;
557
+ }
558
+ if (config.enableDpop && !dpopKey) {
559
+ dpopKey = await createDpopKey();
560
+ }
561
+ const res = await fetch(`${coreBaseUrl}${endpoint}`, {
562
+ method: "POST",
563
+ headers: {
564
+ "Content-Type": "application/json",
565
+ ...config.publishableKey ? { "X-Publishable-Key": config.publishableKey } : {}
566
+ },
567
+ credentials: config.publishableKey ? "omit" : "include",
568
+ body: JSON.stringify({
569
+ projectId: this.projectId,
570
+ dpopJwk: dpopKey?.publicJwk
571
+ })
572
+ });
573
+ const json = await res.json().catch(() => ({}));
574
+ if (!res.ok) {
575
+ throw new Error((typeof json.error === "string" ? json.error : "") || "Failed to fetch token");
576
+ }
577
+ tokenState.value = String(json.token || "");
578
+ tokenState.expiresAt = Date.now() + Number(json.expiresIn || 0) * 1e3;
579
+ return tokenState.value;
580
+ }
581
+ throw new Error("Missing apiKey or token configuration");
582
+ };
583
+ const getAuthHeaders = async (params) => {
584
+ if (config.apiKey) {
585
+ return { "X-API-Key": config.apiKey };
586
+ }
587
+ const token = await getToken();
588
+ const headers = { Authorization: `Bearer ${token}` };
589
+ if (config.enableDpop) {
590
+ if (!dpopKey) dpopKey = await createDpopKey();
591
+ headers["DPoP"] = await createDpopProof({ keyPair: dpopKey.keyPair, method: params.method, url: params.url });
592
+ }
593
+ return headers;
594
+ };
595
+ this.engineClient = new HttpClient({ baseUrl: engineBaseUrl, timeout, getAuthHeaders });
596
+ this.coreClient = new HttpClient({ baseUrl: coreBaseUrl, timeout, getAuthHeaders });
597
+ this._data = new DataModule(this.engineClient);
598
+ this.storage = new StorageModule(this.coreClient, this.projectId);
599
+ this.email = new EmailModule(this.coreClient, this.projectId);
600
+ this.ai = new AIModule(this.coreClient, this.projectId);
539
601
  }
540
602
  /**
541
603
  * Start a query on a table
@@ -547,7 +609,10 @@ var Scarlet = class {
547
609
  return this._data.from(table);
548
610
  }
549
611
  };
612
+ function createScarlet(config) {
613
+ return new Scarlet(config);
614
+ }
550
615
 
551
- export { AuthenticationError, NetworkError, NotFoundError, QueryBuilder, RateLimitError, Scarlet, ScarletError, ValidationError };
616
+ export { AuthenticationError, NetworkError, NotFoundError, QueryBuilder, RateLimitError, Scarlet, ScarletError, ValidationError, createScarlet };
552
617
  //# sourceMappingURL=index.mjs.map
553
618
  //# sourceMappingURL=index.mjs.map