@giaeulate/baas-sdk 1.2.0 → 1.3.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.cjs CHANGED
@@ -30,14 +30,28 @@ module.exports = __toCommonJS(index_exports);
30
30
  var HttpClient = class {
31
31
  url;
32
32
  apiKey = "";
33
+ keyType;
33
34
  token = null;
34
35
  environment = "prod";
35
36
  _onForceLogout = null;
36
- constructor(url, apiKey) {
37
+ constructor(url, apiKey, options) {
37
38
  let baseUrl = url || (typeof window !== "undefined" ? window.location.origin : "http://localhost:8080");
38
39
  this.url = baseUrl.endsWith("/") ? baseUrl.slice(0, -1) : baseUrl;
39
40
  if (apiKey) this.apiKey = apiKey;
40
- this.token = this.cleanValue(localStorage.getItem("baas_token"));
41
+ this.keyType = options?.keyType ?? "service_role";
42
+ this.warnIfUnsafeKey();
43
+ this.token = typeof localStorage !== "undefined" ? this.cleanValue(localStorage.getItem("baas_token")) : null;
44
+ }
45
+ /** SECURITY: a service_role key bypasses RLS and grants admin over every
46
+ * tenant. If it ends up in a browser bundle, anyone can extract it. Warn
47
+ * loudly when a non-anon key is constructed in a browser context. */
48
+ warnIfUnsafeKey() {
49
+ const inBrowser = typeof window !== "undefined" && typeof document !== "undefined";
50
+ if (inBrowser && this.apiKey && this.keyType !== "anon") {
51
+ console.warn(
52
+ '[baas-sdk] SECURITY WARNING: a non-anon (service_role) key is running in a browser. It bypasses Row-Level Security and grants admin over ALL tenants, and is extractable from the bundle. Mint an anon key in the dashboard and construct the client with { keyType: "anon" }.'
53
+ );
54
+ }
41
55
  }
42
56
  /**
43
57
  * Set the auth token manually (e.g. restoring from SecureStore in React Native).
@@ -114,7 +128,14 @@ var HttpClient = class {
114
128
  if (res.status === 403 && data?.error === "ip_not_allowed") {
115
129
  this.handleIPBlocked(data.ip);
116
130
  }
117
- return { error: data?.error || `Request failed: ${res.status}`, ...data };
131
+ const rateLimited = res.status === 429;
132
+ const retryHint = res.headers.get("retry-after") || res.headers.get("x-ratelimit-reset");
133
+ return {
134
+ error: data?.error || `Request failed: ${res.status}`,
135
+ status: res.status,
136
+ ...rateLimited ? { rateLimited: true, retryAfter: retryHint ? parseInt(retryHint, 10) : void 0 } : {},
137
+ ...data
138
+ };
118
139
  }
119
140
  return data;
120
141
  }
@@ -231,10 +252,49 @@ var QueryBuilder = class {
231
252
  this.queryParams.set(column, `not.${operator}.${value}`);
232
253
  return this;
233
254
  }
255
+ /** Array/jsonb/range contains (@>). Arrays become a `{a,b}` literal. */
256
+ contains(column, values) {
257
+ this.queryParams.set(column, `cs.${this.arrayLiteral(values)}`);
258
+ return this;
259
+ }
260
+ /** Array/jsonb/range contained-by (<@). */
261
+ containedBy(column, values) {
262
+ this.queryParams.set(column, `cd.${this.arrayLiteral(values)}`);
263
+ return this;
264
+ }
265
+ /** Array/range overlap (&&). */
266
+ overlaps(column, values) {
267
+ this.queryParams.set(column, `ov.${this.arrayLiteral(values)}`);
268
+ return this;
269
+ }
270
+ /** Full-text search (@@). type: fts|plfts|phfts|wfts (tsquery flavour). */
271
+ textSearch(column, query, type = "fts") {
272
+ this.queryParams.set(column, `${type}.${query}`);
273
+ return this;
274
+ }
275
+ arrayLiteral(values) {
276
+ return Array.isArray(values) ? `{${values.join(",")}}` : `${values}`;
277
+ }
278
+ /** OR group: `or=(c1,c2)`. Members are dotted `col.op.value` strings. */
234
279
  or(filters) {
235
280
  this.queryParams.set("or", `(${filters})`);
236
281
  return this;
237
282
  }
283
+ /** AND group: `and=(c1,c2)` — e.g. a range `qty.gt.5,qty.lt.20`. */
284
+ and(filters) {
285
+ this.queryParams.set("and", `(${filters})`);
286
+ return this;
287
+ }
288
+ /** Negated AND group: `not.and=(...)`. */
289
+ notAnd(filters) {
290
+ this.queryParams.set("not.and", `(${filters})`);
291
+ return this;
292
+ }
293
+ /** Negated OR group: `not.or=(...)`. */
294
+ notOr(filters) {
295
+ this.queryParams.set("not.or", `(${filters})`);
296
+ return this;
297
+ }
238
298
  order(column, { ascending = true } = {}) {
239
299
  this.queryParams.set("order", `${column}.${ascending ? "asc" : "desc"}`);
240
300
  return this;
@@ -278,6 +338,34 @@ var QueryBuilder = class {
278
338
  if (res.status === 204) return { success: true };
279
339
  return await this.handleResponse(res);
280
340
  }
341
+ /** Insert-or-update on conflict. [onConflict] is the conflict-target column(s). */
342
+ async upsert(data, onConflict) {
343
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}/upsert`, {
344
+ method: "POST",
345
+ headers: this.getHeaders(),
346
+ body: JSON.stringify({ ...data, on_conflict: onConflict })
347
+ });
348
+ return await this.handleResponse(res);
349
+ }
350
+ /**
351
+ * Count rows matching the current filters (PostgREST parity). Sends
352
+ * `Prefer: count=exact` and parses the total from the `Content-Range`
353
+ * response header. Returns `{ count, error, status }`.
354
+ */
355
+ async count() {
356
+ const res = await fetch(`${this.url}/api/v1/data/${this.table}?${this.queryParams.toString()}`, {
357
+ headers: { ...this.getHeaders(), Prefer: "count=exact", "Cache-Control": "no-cache" },
358
+ cache: "no-store"
359
+ });
360
+ if (!res.ok) {
361
+ if (res.status === 401) this.client.forceLogout();
362
+ const body = await res.json().catch(() => null);
363
+ return { count: 0, error: body?.error || "Request failed", status: res.status };
364
+ }
365
+ const cr = res.headers.get("content-range");
366
+ const total = cr && cr.includes("/") ? parseInt(cr.split("/").pop() || "", 10) : NaN;
367
+ return { count: Number.isNaN(total) ? 0 : total, error: null, status: res.status };
368
+ }
281
369
  getHeaders() {
282
370
  return this.client.getHeaders();
283
371
  }
@@ -333,6 +421,43 @@ function createAuthModule(client) {
333
421
  localStorage.removeItem("baas_token");
334
422
  }
335
423
  },
424
+ // Anonymous sign-in (Supabase parity). Response carries access_token (+ a
425
+ // back-compat `token`); store whichever is present.
426
+ async signInAnonymous() {
427
+ const data = await post("/api/auth/anonymous");
428
+ const tok = data?.access_token || data?.token;
429
+ if (tok) {
430
+ client.token = tok;
431
+ localStorage.setItem("baas_token", tok);
432
+ }
433
+ return data;
434
+ },
435
+ // Passwordless: request a magic link + 6-digit email OTP. Always resolves on
436
+ // a well-formed request (no account enumeration).
437
+ async requestOtp(email, createUser = true) {
438
+ return post("/api/auth/otp", { email, create_user: createUser });
439
+ },
440
+ // Verify a passwordless OTP/magic-link and complete sign-in.
441
+ async verifyOtp(type, email, token) {
442
+ const data = await post("/api/auth/verify", { type, email, token });
443
+ const tok = data?.access_token || data?.token;
444
+ if (tok) {
445
+ client.token = tok;
446
+ localStorage.setItem("baas_token", tok);
447
+ }
448
+ return data;
449
+ },
450
+ // Upgrade the current anonymous guest to a real account (preserves user id).
451
+ // Requires the anonymous Bearer to be set (call after signInAnonymous).
452
+ async upgradeAnonymous(email, password) {
453
+ const data = await post("/api/auth/upgrade", { email, password });
454
+ const tok = data?.access_token || data?.token;
455
+ if (tok) {
456
+ client.token = tok;
457
+ localStorage.setItem("baas_token", tok);
458
+ }
459
+ return data;
460
+ },
336
461
  async verifyMFA(mfaToken, code) {
337
462
  const data = await post("/api/mfa/finalize", { mfa_token: mfaToken, code });
338
463
  if (data.token) {
@@ -530,6 +655,7 @@ function createDatabaseModule(client) {
530
655
  function createStorageModule(client) {
531
656
  const get = (endpoint) => client.get(endpoint);
532
657
  const post = (endpoint, body) => client.post(endpoint, body);
658
+ const put = (endpoint, body) => client.put(endpoint, body);
533
659
  const del = (endpoint) => client.delete(endpoint);
534
660
  async function uploadFile(fileOrTable, fileOrBucketId) {
535
661
  let file;
@@ -564,8 +690,15 @@ function createStorageModule(client) {
564
690
  async listFiles() {
565
691
  return get("/api/storage/files");
566
692
  },
567
- async getFile(fileId) {
568
- return get(`/api/storage/files/${fileId}`);
693
+ async getFile(fileId, expiresIn) {
694
+ const q = expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : "";
695
+ return get(`/api/storage/files/${fileId}${q}`);
696
+ },
697
+ async createSignedUrl(fileId, expiresIn) {
698
+ const res = await get(
699
+ `/api/storage/files/${fileId}${expiresIn && expiresIn > 0 ? `?expiresIn=${expiresIn}` : ""}`
700
+ );
701
+ return { signedUrl: res?.url };
569
702
  },
570
703
  async deleteFile(fileId) {
571
704
  return del(`/api/storage/files/${fileId}`);
@@ -573,6 +706,9 @@ function createStorageModule(client) {
573
706
  async createBucket(name, isPublic = false) {
574
707
  return post("/api/storage/buckets", { name, is_public: isPublic });
575
708
  },
709
+ async updateBucket(bucketId, opts) {
710
+ return put(`/api/storage/buckets/${bucketId}`, { is_public: opts.isPublic });
711
+ },
576
712
  async listBuckets() {
577
713
  return get("/api/storage/buckets");
578
714
  },
@@ -584,6 +720,17 @@ function createStorageModule(client) {
584
720
  },
585
721
  async listBucketFiles(bucketId) {
586
722
  return get(`/api/storage/buckets/${bucketId}/files`);
723
+ },
724
+ async download(fileId) {
725
+ const res = await fetch(`${client.url}/api/storage/files/${fileId}/download`, {
726
+ headers: client.getHeaders("")
727
+ });
728
+ if (!res.ok) {
729
+ if (res.status === 401) client.forceLogout();
730
+ const err = await res.json().catch(() => ({ error: `Download failed: ${res.status}` }));
731
+ throw new Error(err.error ?? `Download failed: ${res.status}`);
732
+ }
733
+ return res.blob();
587
734
  }
588
735
  };
589
736
  }
@@ -684,7 +831,9 @@ function createFunctionsModule(client) {
684
831
  return del(`/api/functions/${id}`);
685
832
  },
686
833
  async update(id, code, options) {
687
- return put(`/api/functions/${id}`, { code, ...options });
834
+ const current = await get(`/api/functions/${id}`);
835
+ const name = current?.name ?? current?.data?.name;
836
+ return post("/api/functions", { name, code, ...options });
688
837
  },
689
838
  async executeCode(code, options) {
690
839
  return post("/api/functions/execute", { code, ...options });
@@ -830,7 +979,9 @@ function createGraphQLModule(client) {
830
979
  return post("/api/graphql", { query, variables });
831
980
  },
832
981
  async getSchema() {
833
- return get("/api/graphql/schema");
982
+ return post("/api/graphql", {
983
+ query: "{ __schema { queryType { name } mutationType { name } types { name kind } } }"
984
+ });
834
985
  },
835
986
  getPlaygroundUrl() {
836
987
  return `${client.url}/api/graphql/playground`;
@@ -876,6 +1027,7 @@ function createMetricsModule(client) {
876
1027
  // src/modules/audit.ts
877
1028
  function createAuditModule(client) {
878
1029
  const get = (endpoint) => client.get(endpoint);
1030
+ const del = (endpoint) => client.delete(endpoint);
879
1031
  return {
880
1032
  async list(options) {
881
1033
  const params = new URLSearchParams();
@@ -892,6 +1044,40 @@ function createAuditModule(client) {
892
1044
  },
893
1045
  async getActions() {
894
1046
  return get("/api/audit/actions");
1047
+ },
1048
+ async exportLogs(format, options) {
1049
+ const params = new URLSearchParams();
1050
+ params.set("format", format);
1051
+ if (options?.action) params.set("action", options.action);
1052
+ if (options?.method) params.set("method", options.method);
1053
+ if (options?.path) params.set("path", options.path);
1054
+ if (options?.status_code) params.set("status_code", options.status_code.toString());
1055
+ if (options?.start_date) params.set("start_date", options.start_date);
1056
+ if (options?.end_date) params.set("end_date", options.end_date);
1057
+ const res = await fetch(`${client.url}/api/audit/export?${params.toString()}`, {
1058
+ headers: client.getHeaders(),
1059
+ credentials: "include"
1060
+ });
1061
+ if (!res.ok) {
1062
+ const data = await res.json();
1063
+ return { error: data.error || `Export failed: ${res.status}` };
1064
+ }
1065
+ const blob = await res.blob();
1066
+ const url = window.URL.createObjectURL(blob);
1067
+ const a = document.createElement("a");
1068
+ a.href = url;
1069
+ a.download = `audit_logs_${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}.${format}`;
1070
+ document.body.appendChild(a);
1071
+ a.click();
1072
+ window.URL.revokeObjectURL(url);
1073
+ document.body.removeChild(a);
1074
+ return { success: true };
1075
+ },
1076
+ async purgePreview(olderThanDays) {
1077
+ return get(`/api/audit/purge/preview?older_than_days=${olderThanDays}`);
1078
+ },
1079
+ async purge(olderThanDays) {
1080
+ return del(`/api/audit/purge?older_than_days=${olderThanDays}`);
895
1081
  }
896
1082
  };
897
1083
  }
@@ -1017,6 +1203,9 @@ var RealtimeService = class {
1017
1203
  socket.onopen = () => {
1018
1204
  console.log("BaaS Realtime: Connected");
1019
1205
  this.reconnectAttempts = 0;
1206
+ for (const table of this.subscribers.keys()) {
1207
+ this.sendSub("subscribe", table);
1208
+ }
1020
1209
  resolve();
1021
1210
  };
1022
1211
  socket.onmessage = (event) => {
@@ -1054,11 +1243,15 @@ var RealtimeService = class {
1054
1243
  */
1055
1244
  async subscribe(table, action, callback) {
1056
1245
  await this.connect();
1057
- if (!this.subscribers.has(table)) {
1246
+ const isNewTable = !this.subscribers.has(table);
1247
+ if (isNewTable) {
1058
1248
  this.subscribers.set(table, /* @__PURE__ */ new Set());
1059
1249
  }
1060
1250
  const sub = { action, callback };
1061
1251
  this.subscribers.get(table).add(sub);
1252
+ if (isNewTable && table !== "*") {
1253
+ this.sendSub("subscribe", table);
1254
+ }
1062
1255
  return {
1063
1256
  unsubscribe: () => {
1064
1257
  const tableSubs = this.subscribers.get(table);
@@ -1066,11 +1259,18 @@ var RealtimeService = class {
1066
1259
  tableSubs.delete(sub);
1067
1260
  if (tableSubs.size === 0) {
1068
1261
  this.subscribers.delete(table);
1262
+ if (table !== "*") this.sendSub("unsubscribe", table);
1069
1263
  }
1070
1264
  }
1071
1265
  }
1072
1266
  };
1073
1267
  }
1268
+ /** Send a subscribe/unsubscribe control frame to the server. */
1269
+ sendSub(event, table) {
1270
+ if (this.socket?.readyState === WebSocket.OPEN) {
1271
+ this.socket.send(JSON.stringify({ event, table }));
1272
+ }
1273
+ }
1074
1274
  /**
1075
1275
  * Handle incoming CDC events from the server
1076
1276
  */
@@ -1242,6 +1442,44 @@ function createIPWhitelistModule(client) {
1242
1442
  };
1243
1443
  }
1244
1444
 
1445
+ // src/modules/cache.ts
1446
+ function createCacheModule(client) {
1447
+ const get = (endpoint) => client.get(endpoint);
1448
+ const post = (endpoint, body) => client.post(endpoint, body);
1449
+ const patch = (endpoint, body) => client.patch(endpoint, body);
1450
+ const del = (endpoint) => client.delete(endpoint);
1451
+ return {
1452
+ async getStatus() {
1453
+ return get("/api/cache/status");
1454
+ },
1455
+ async getStats() {
1456
+ return get("/api/cache/stats");
1457
+ },
1458
+ async listPolicies() {
1459
+ return get("/api/cache/policies");
1460
+ },
1461
+ async updatePolicy(namespace, body) {
1462
+ return patch(`/api/cache/policies/${encodeURIComponent(namespace)}`, body);
1463
+ },
1464
+ async removePolicy(namespace) {
1465
+ return del(`/api/cache/policies/${encodeURIComponent(namespace)}`);
1466
+ },
1467
+ async invalidateNamespace(namespace) {
1468
+ return post(`/api/cache/invalidate/${encodeURIComponent(namespace)}`);
1469
+ },
1470
+ async invalidateAll() {
1471
+ return post("/api/cache/invalidate-all");
1472
+ },
1473
+ async listKeys(prefix, cursor = 0, count = 100) {
1474
+ const params = new URLSearchParams({ prefix, cursor: String(cursor), count: String(count) });
1475
+ return get(`/api/cache/keys?${params}`);
1476
+ },
1477
+ async inspectKey(key) {
1478
+ return get(`/api/cache/keys/inspect?key=${encodeURIComponent(key)}`);
1479
+ }
1480
+ };
1481
+ }
1482
+
1245
1483
  // src/client.ts
1246
1484
  var BaasClient = class extends HttpClient {
1247
1485
  // Feature modules
@@ -1268,8 +1506,9 @@ var BaasClient = class extends HttpClient {
1268
1506
  corsOrigins;
1269
1507
  policies;
1270
1508
  ipWhitelist;
1271
- constructor(url, apiKey) {
1272
- super(url, apiKey);
1509
+ cache;
1510
+ constructor(url, apiKey, options) {
1511
+ super(url, apiKey, options);
1273
1512
  this.auth = createAuthModule(this);
1274
1513
  this.users = createUsersModule(this);
1275
1514
  this.database = createDatabaseModule(this);
@@ -1293,6 +1532,7 @@ var BaasClient = class extends HttpClient {
1293
1532
  this.corsOrigins = createCorsOriginsModule(this);
1294
1533
  this.policies = createPoliciesModule(this);
1295
1534
  this.ipWhitelist = createIPWhitelistModule(this);
1535
+ this.cache = createCacheModule(this);
1296
1536
  }
1297
1537
  /**
1298
1538
  * Create a query builder for fluent data queries
@@ -1300,6 +1540,18 @@ var BaasClient = class extends HttpClient {
1300
1540
  from(table) {
1301
1541
  return new QueryBuilder(table, this.url, this);
1302
1542
  }
1543
+ /**
1544
+ * Invoke a PL/pgSQL function (PostgREST `.rpc()` parity). Runs under the
1545
+ * caller's RLS context; args bind by name and are $N-safe. Pass [params] for
1546
+ * POST (named args in the body) or set `opts.get = true` for the GET variant.
1547
+ */
1548
+ async rpc(fn, params, opts) {
1549
+ if (opts?.get) {
1550
+ const qs = params ? "?" + new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString() : "";
1551
+ return this.get(`/api/v1/rpc/${fn}${qs}`);
1552
+ }
1553
+ return this.post(`/api/v1/rpc/${fn}`, params ?? {});
1554
+ }
1303
1555
  // ============================================
1304
1556
  // BACKWARD COMPATIBILITY METHODS
1305
1557
  // These delegate to the appropriate modules
@@ -1667,6 +1919,15 @@ var BaasClient = class extends HttpClient {
1667
1919
  async getAuditActions() {
1668
1920
  return this.audit.getActions();
1669
1921
  }
1922
+ async exportAuditLogs(format, options) {
1923
+ return this.audit.exportLogs(format, options);
1924
+ }
1925
+ async previewPurgeAuditLogs(olderThanDays) {
1926
+ return this.audit.purgePreview(olderThanDays);
1927
+ }
1928
+ async purgeAuditLogs(olderThanDays) {
1929
+ return this.audit.purge(olderThanDays);
1930
+ }
1670
1931
  // Realtime shortcuts
1671
1932
  subscribe(table, action, callback) {
1672
1933
  return this.realtime.subscribe(table, action, callback);
@@ -1721,3 +1982,4 @@ var BaasClient = class extends HttpClient {
1721
1982
  HttpClient,
1722
1983
  QueryBuilder
1723
1984
  });
1985
+ //# sourceMappingURL=index.cjs.map