@mitway/sdk 0.3.0 → 0.4.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/README.md CHANGED
@@ -73,6 +73,22 @@ const { data: newPost } = await client.database
73
73
  const { data: stats } = await client.database
74
74
  .rpc('get_user_stats', { user_id: session.user.id });
75
75
 
76
+ // Upload a file
77
+ const { data: uploaded, error: upErr } = await client.storage
78
+ .from('avatars')
79
+ .upload('user-123/avatar.png', file, { contentType: 'image/png' });
80
+
81
+ // Share a link that expires in an hour
82
+ const { data: signed } = await client.storage
83
+ .from('private-docs')
84
+ .createSignedUrl('contract.pdf', { expiresIn: 3600 });
85
+ console.log(signed?.url);
86
+
87
+ // Public download URL (for public buckets)
88
+ const { data: pub } = client.storage
89
+ .from('avatars')
90
+ .getPublicUrl('user-123/avatar.png');
91
+
76
92
  // Sign out
77
93
  await client.auth.signOut();
78
94
  ```
package/dist/index.cjs CHANGED
@@ -28,6 +28,8 @@ __export(index_exports, {
28
28
  MitwayBaasError: () => MitwayBaasError,
29
29
  Realtime: () => Realtime,
30
30
  RealtimeChannel: () => RealtimeChannel,
31
+ Storage: () => Storage,
32
+ StorageBucketClient: () => StorageBucketClient,
31
33
  TokenManager: () => TokenManager,
32
34
  createClient: () => createClient,
33
35
  default: () => index_default
@@ -564,6 +566,29 @@ var HttpClient = class {
564
566
  throw error;
565
567
  }
566
568
  }
569
+ /**
570
+ * Low-level fetch helper for binary bodies (uploads) and streamed responses
571
+ * (downloads). Applies the current Bearer token (user session → anon key
572
+ * fallback) plus any configured default headers, resolves `path` against
573
+ * `baseUrl`, and returns the raw `Response` — it does NOT unwrap the
574
+ * `{ data, error }` envelope, so the caller is responsible for status
575
+ * checking and parsing.
576
+ *
577
+ * Used by the storage module for object upload/download paths where the
578
+ * body or response is not JSON.
579
+ */
580
+ async rawFetch(path, init = {}) {
581
+ const url = this.buildUrl(path);
582
+ const headers = new Headers(init.headers ?? {});
583
+ for (const [k, v] of Object.entries(this.defaultHeaders)) {
584
+ if (!headers.has(k)) headers.set(k, v);
585
+ }
586
+ if (!headers.has("Authorization")) {
587
+ const token = this.userToken ?? this.anonKey;
588
+ if (token) headers.set("Authorization", `Bearer ${token}`);
589
+ }
590
+ return this.fetch(url, { ...init, headers });
591
+ }
567
592
  get(path, options) {
568
593
  return this.request("GET", path, options);
569
594
  }
@@ -1513,6 +1538,373 @@ var Realtime = class {
1513
1538
  }
1514
1539
  };
1515
1540
 
1541
+ // src/modules/storage.ts
1542
+ function bucketFromWire(row) {
1543
+ return {
1544
+ id: row.id,
1545
+ name: row.name,
1546
+ public: row.public,
1547
+ fileSizeLimitBytes: row.file_size_limit_bytes,
1548
+ allowedMimeTypes: row.allowed_mime_types,
1549
+ createdAt: row.created_at,
1550
+ updatedAt: row.updated_at
1551
+ };
1552
+ }
1553
+ function objectFromWire(row, bucketName) {
1554
+ return {
1555
+ id: row.id,
1556
+ bucket: bucketName,
1557
+ key: row.key,
1558
+ size: row.size,
1559
+ mimeType: row.mime_type,
1560
+ etag: row.etag,
1561
+ cacheControl: row.cache_control,
1562
+ contentDisposition: row.content_disposition,
1563
+ uploadedBy: row.uploaded_by,
1564
+ uploadedAt: row.uploaded_at,
1565
+ updatedAt: row.updated_at
1566
+ };
1567
+ }
1568
+ function configFromWire(row) {
1569
+ return {
1570
+ defaultFileSizeLimitBytes: row.default_file_size_limit_bytes,
1571
+ maxFileSizeLimitBytes: row.max_file_size_limit_bytes,
1572
+ tenantStorageQuotaBytes: row.tenant_storage_quota_bytes,
1573
+ reservedSpaceBytes: row.reserved_space_bytes,
1574
+ signedUrlDefaultTtlSec: row.signed_url_default_ttl_sec,
1575
+ signedUrlMaxTtlSec: row.signed_url_max_ttl_sec
1576
+ };
1577
+ }
1578
+ function encodeKey(key) {
1579
+ return key.split("/").map(encodeURIComponent).join("/");
1580
+ }
1581
+ function wrapError2(err, fallback) {
1582
+ if (err instanceof MitwayBaasError) return { data: null, error: err };
1583
+ return {
1584
+ data: null,
1585
+ error: new MitwayBaasError(
1586
+ err instanceof Error ? err.message : fallback,
1587
+ 0,
1588
+ "STORAGE_ERROR"
1589
+ )
1590
+ };
1591
+ }
1592
+ async function readEnvelopeError(response) {
1593
+ let code = "STORAGE_ERROR";
1594
+ let message = `HTTP ${response.status}`;
1595
+ try {
1596
+ const body = await response.json();
1597
+ if (body && body.error) {
1598
+ code = body.error.code ?? code;
1599
+ message = body.error.message ?? message;
1600
+ }
1601
+ } catch {
1602
+ }
1603
+ return new MitwayBaasError(message, response.status, code);
1604
+ }
1605
+ var StorageBucketClient = class {
1606
+ constructor(http, bucketName) {
1607
+ this.http = http;
1608
+ this.bucketName = bucketName;
1609
+ }
1610
+ http;
1611
+ bucketName;
1612
+ bucketBase() {
1613
+ return `/api/storage/buckets/${encodeURIComponent(this.bucketName)}`;
1614
+ }
1615
+ objectPath(key) {
1616
+ return `${this.bucketBase()}/objects/${encodeKey(key)}`;
1617
+ }
1618
+ async upload(key, body, opts = {}) {
1619
+ try {
1620
+ const method = opts.upsert ? "PUT" : "POST";
1621
+ const headers = {
1622
+ "Content-Type": opts.contentType ?? "application/octet-stream"
1623
+ };
1624
+ if (opts.cacheControl) headers["Cache-Control"] = opts.cacheControl;
1625
+ if (opts.contentDisposition)
1626
+ headers["Content-Disposition"] = opts.contentDisposition;
1627
+ const response = await this.http.rawFetch(this.objectPath(key), {
1628
+ method,
1629
+ headers,
1630
+ body,
1631
+ signal: opts.abortSignal
1632
+ });
1633
+ if (!response.ok) {
1634
+ return { data: null, error: await readEnvelopeError(response) };
1635
+ }
1636
+ const parsed = await response.json();
1637
+ if (parsed.error || !parsed.data) {
1638
+ return {
1639
+ data: null,
1640
+ error: new MitwayBaasError(
1641
+ parsed.error?.message ?? "Upload failed",
1642
+ response.status,
1643
+ parsed.error?.code ?? "STORAGE_ERROR"
1644
+ )
1645
+ };
1646
+ }
1647
+ return { data: objectFromWire(parsed.data, this.bucketName), error: null };
1648
+ } catch (err) {
1649
+ return wrapError2(err, "Upload failed");
1650
+ }
1651
+ }
1652
+ async download(key, opts = {}) {
1653
+ try {
1654
+ const headers = {};
1655
+ if (opts.range) {
1656
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1657
+ }
1658
+ const response = await this.http.rawFetch(this.objectPath(key), {
1659
+ method: "GET",
1660
+ headers,
1661
+ signal: opts.abortSignal
1662
+ });
1663
+ if (!response.ok) {
1664
+ return { data: null, error: await readEnvelopeError(response) };
1665
+ }
1666
+ const blob = await response.blob();
1667
+ return { data: blob, error: null };
1668
+ } catch (err) {
1669
+ return wrapError2(err, "Download failed");
1670
+ }
1671
+ }
1672
+ async getStream(key, opts = {}) {
1673
+ try {
1674
+ const headers = {};
1675
+ if (opts.range) {
1676
+ headers["Range"] = `bytes=${opts.range.start}-${opts.range.end}`;
1677
+ }
1678
+ const response = await this.http.rawFetch(this.objectPath(key), {
1679
+ method: "GET",
1680
+ headers,
1681
+ signal: opts.abortSignal
1682
+ });
1683
+ if (!response.ok) {
1684
+ return { data: null, error: await readEnvelopeError(response) };
1685
+ }
1686
+ if (!response.body) {
1687
+ return {
1688
+ data: null,
1689
+ error: new MitwayBaasError(
1690
+ "Response body is not a stream",
1691
+ response.status,
1692
+ "STORAGE_ERROR"
1693
+ )
1694
+ };
1695
+ }
1696
+ return {
1697
+ data: response.body,
1698
+ error: null
1699
+ };
1700
+ } catch (err) {
1701
+ return wrapError2(err, "Download failed");
1702
+ }
1703
+ }
1704
+ async remove(keys) {
1705
+ try {
1706
+ const results = await Promise.allSettled(
1707
+ keys.map(
1708
+ (key) => this.http.rawFetch(this.objectPath(key), { method: "DELETE" })
1709
+ )
1710
+ );
1711
+ const removed = [];
1712
+ const errors = [];
1713
+ for (let i = 0; i < keys.length; i++) {
1714
+ const key = keys[i];
1715
+ const r = results[i];
1716
+ if (r.status === "fulfilled" && r.value.ok) {
1717
+ removed.push(key);
1718
+ } else if (r.status === "fulfilled") {
1719
+ errors.push(`${key}: HTTP ${r.value.status}`);
1720
+ } else {
1721
+ const msg = r.reason instanceof Error ? r.reason.message : String(r.reason);
1722
+ errors.push(`${key}: ${msg}`);
1723
+ }
1724
+ }
1725
+ if (errors.length > 0) {
1726
+ return {
1727
+ data: null,
1728
+ error: new MitwayBaasError(
1729
+ `Failed to delete some objects: ${errors.join("; ")}`,
1730
+ 0,
1731
+ "STORAGE_ERROR"
1732
+ )
1733
+ };
1734
+ }
1735
+ return { data: { removed }, error: null };
1736
+ } catch (err) {
1737
+ return wrapError2(err, "Delete failed");
1738
+ }
1739
+ }
1740
+ async list(opts = {}) {
1741
+ try {
1742
+ const params = {};
1743
+ if (opts.prefix !== void 0) params.prefix = opts.prefix;
1744
+ if (opts.limit !== void 0) params.limit = String(opts.limit);
1745
+ if (opts.startAfter !== void 0) params.start_after = opts.startAfter;
1746
+ const rows = await this.http.get(
1747
+ `${this.bucketBase()}/objects`,
1748
+ { params }
1749
+ );
1750
+ return {
1751
+ data: rows.map((r) => objectFromWire(r, this.bucketName)),
1752
+ error: null
1753
+ };
1754
+ } catch (err) {
1755
+ return wrapError2(err, "List failed");
1756
+ }
1757
+ }
1758
+ async copy(fromKey, toKey, toBucket) {
1759
+ try {
1760
+ const row = await this.http.post(
1761
+ `${this.objectPath(fromKey)}/copy`,
1762
+ {
1763
+ dest_bucket: toBucket ?? this.bucketName,
1764
+ dest_key: toKey
1765
+ }
1766
+ );
1767
+ return {
1768
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1769
+ error: null
1770
+ };
1771
+ } catch (err) {
1772
+ return wrapError2(err, "Copy failed");
1773
+ }
1774
+ }
1775
+ async move(fromKey, toKey, toBucket) {
1776
+ try {
1777
+ const row = await this.http.post(
1778
+ `${this.objectPath(fromKey)}/move`,
1779
+ {
1780
+ dest_bucket: toBucket ?? this.bucketName,
1781
+ dest_key: toKey
1782
+ }
1783
+ );
1784
+ return {
1785
+ data: objectFromWire(row, toBucket ?? this.bucketName),
1786
+ error: null
1787
+ };
1788
+ } catch (err) {
1789
+ return wrapError2(err, "Move failed");
1790
+ }
1791
+ }
1792
+ async createSignedUrl(key, opts = {}) {
1793
+ try {
1794
+ const body = {};
1795
+ if (opts.expiresIn !== void 0) body.expires_in = opts.expiresIn;
1796
+ const wire = await this.http.post(`${this.objectPath(key)}/sign`, body);
1797
+ return {
1798
+ data: {
1799
+ url: wire.url,
1800
+ token: wire.token,
1801
+ expiresAt: wire.expiresAt
1802
+ },
1803
+ error: null
1804
+ };
1805
+ } catch (err) {
1806
+ return wrapError2(err, "Sign failed");
1807
+ }
1808
+ }
1809
+ getPublicUrl(key) {
1810
+ const url = `${this.http.baseUrl.replace(/\/$/, "")}${this.objectPath(key)}`;
1811
+ return { data: { url } };
1812
+ }
1813
+ };
1814
+ var Storage = class {
1815
+ constructor(http) {
1816
+ this.http = http;
1817
+ }
1818
+ http;
1819
+ /** Scope subsequent operations to a single bucket. */
1820
+ from(bucketName) {
1821
+ return new StorageBucketClient(this.http, bucketName);
1822
+ }
1823
+ // --- Admin (require service_role) ---
1824
+ async listBuckets() {
1825
+ try {
1826
+ const rows = await this.http.get("/api/storage/buckets");
1827
+ return { data: rows.map(bucketFromWire), error: null };
1828
+ } catch (err) {
1829
+ return wrapError2(err, "listBuckets failed");
1830
+ }
1831
+ }
1832
+ async getBucket(name) {
1833
+ try {
1834
+ const row = await this.http.get(
1835
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1836
+ );
1837
+ return { data: bucketFromWire(row), error: null };
1838
+ } catch (err) {
1839
+ return wrapError2(err, "getBucket failed");
1840
+ }
1841
+ }
1842
+ async createBucket(name, opts = {}) {
1843
+ try {
1844
+ const body = { name };
1845
+ if (opts.public !== void 0) body.public = opts.public;
1846
+ if (opts.fileSizeLimitBytes !== void 0)
1847
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1848
+ if (opts.allowedMimeTypes !== void 0)
1849
+ body.allowed_mime_types = opts.allowedMimeTypes;
1850
+ const row = await this.http.post(
1851
+ "/api/storage/buckets",
1852
+ body
1853
+ );
1854
+ return { data: bucketFromWire(row), error: null };
1855
+ } catch (err) {
1856
+ return wrapError2(err, "createBucket failed");
1857
+ }
1858
+ }
1859
+ async updateBucket(name, opts) {
1860
+ try {
1861
+ const body = {};
1862
+ if (opts.public !== void 0) body.public = opts.public;
1863
+ if (opts.fileSizeLimitBytes !== void 0)
1864
+ body.file_size_limit_bytes = opts.fileSizeLimitBytes;
1865
+ if (opts.allowedMimeTypes !== void 0)
1866
+ body.allowed_mime_types = opts.allowedMimeTypes;
1867
+ const row = await this.http.patch(
1868
+ `/api/storage/buckets/${encodeURIComponent(name)}`,
1869
+ body
1870
+ );
1871
+ return { data: bucketFromWire(row), error: null };
1872
+ } catch (err) {
1873
+ return wrapError2(err, "updateBucket failed");
1874
+ }
1875
+ }
1876
+ async deleteBucket(name) {
1877
+ try {
1878
+ await this.http.delete(
1879
+ `/api/storage/buckets/${encodeURIComponent(name)}`
1880
+ );
1881
+ return { data: null, error: null };
1882
+ } catch (err) {
1883
+ return wrapError2(err, "deleteBucket failed");
1884
+ }
1885
+ }
1886
+ async emptyBucket(name) {
1887
+ try {
1888
+ const result = await this.http.post(
1889
+ `/api/storage/buckets/${encodeURIComponent(name)}/empty`,
1890
+ {}
1891
+ );
1892
+ return { data: result, error: null };
1893
+ } catch (err) {
1894
+ return wrapError2(err, "emptyBucket failed");
1895
+ }
1896
+ }
1897
+ // --- Storage config (service_role) ---
1898
+ async getConfig() {
1899
+ try {
1900
+ const row = await this.http.get("/api/storage/config");
1901
+ return { data: configFromWire(row), error: null };
1902
+ } catch (err) {
1903
+ return wrapError2(err, "getConfig failed");
1904
+ }
1905
+ }
1906
+ };
1907
+
1516
1908
  // src/client.ts
1517
1909
  var MitwayBaasClient = class {
1518
1910
  http;
@@ -1520,6 +1912,7 @@ var MitwayBaasClient = class {
1520
1912
  auth;
1521
1913
  database;
1522
1914
  realtime;
1915
+ storage;
1523
1916
  constructor(config = {}) {
1524
1917
  const logger = new Logger(config.debug);
1525
1918
  this.tokenManager = new TokenManager();
@@ -1532,6 +1925,7 @@ var MitwayBaasClient = class {
1532
1925
  config.anonKey,
1533
1926
  config.realtime
1534
1927
  );
1928
+ this.storage = new Storage(this.http);
1535
1929
  }
1536
1930
  /**
1537
1931
  * Escape hatch for callers that need to make custom requests against the
@@ -1557,6 +1951,8 @@ var index_default = MitwayBaasClient;
1557
1951
  MitwayBaasError,
1558
1952
  Realtime,
1559
1953
  RealtimeChannel,
1954
+ Storage,
1955
+ StorageBucketClient,
1560
1956
  TokenManager,
1561
1957
  createClient
1562
1958
  });