next-supa-utils 0.1.4 → 0.1.5

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
@@ -102,6 +102,30 @@ export default function Avatar() {
102
102
  }
103
103
  ```
104
104
 
105
+ ### 4. Route Handlers (API Routes)
106
+
107
+ Wrap your Next.js API endpoints to automatically handle Supabase errors and enforce authentication.
108
+
109
+ ```ts
110
+ // app/api/posts/route.ts
111
+ import { routeWrapper } from "next-supa-utils/server";
112
+ import { NextResponse } from "next/server";
113
+
114
+ export const POST = routeWrapper(
115
+ async (request, { supabase, user }) => {
116
+ const body = await request.json();
117
+ const { data, error } = await supabase
118
+ .from("posts")
119
+ .insert({ ...body, user_id: user!.id })
120
+ .single();
121
+
122
+ if (error) throw error; // Auto-caught and returned as 500
123
+ return NextResponse.json({ data });
124
+ },
125
+ { requireAuth: true } // Auto-returns 401 if not logged in
126
+ );
127
+ ```
128
+
105
129
  ---
106
130
 
107
131
  ## API Reference
@@ -161,6 +185,29 @@ function createAction<TArgs extends unknown[], TResult>(
161
185
 
162
186
  ---
163
187
 
188
+ #### `routeWrapper(handler, options?)`
189
+
190
+ Wraps a Next.js App Router Route Handler (e.g., `GET`, `POST`) with automatic try-catch, error normalization, and optional authentication gating.
191
+
192
+ **Signature:**
193
+
194
+ ```ts
195
+ function routeWrapper<TContext>(
196
+ handler: (request: NextRequest, context: RouteHandlerContext<TContext>) => Promise<NextResponse | Response>,
197
+ options?: RouteWrapperOptions
198
+ ): (request: NextRequest, context: NextRouteContext) => Promise<NextResponse>
199
+ ```
200
+
201
+ **Options (`RouteWrapperOptions`):**
202
+ - `requireAuth` (`boolean`): If `true`, returns a `401 Unauthorized` response if the user has no valid session.
203
+
204
+ **Context (`RouteHandlerContext`):**
205
+ - `params`: Auto-resolved dynamic route params (e.g., `{ id: "123" }`).
206
+ - `supabase`: An initialized Supabase server client (always available).
207
+ - `user`: The authenticated user (guaranteed non-null if `requireAuth: true`).
208
+
209
+ ---
210
+
164
211
  ### Client — `next-supa-utils/client`
165
212
 
166
213
  > ⚠️ All client exports include the `"use client"` directive. They must be used inside Client Components only.
@@ -199,6 +246,45 @@ React hook that provides the current session (access token, refresh token, expir
199
246
 
200
247
  ---
201
248
 
249
+ #### `useSupaUpload(bucketName)`
250
+
251
+ React hook that simplifies uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to bypass the JS SDK's lack of progress events).
252
+
253
+ **Returns:** `UseSupaUploadReturn`
254
+
255
+ | Property | Type | Description |
256
+ |---|---|---|
257
+ | `upload` | `(file: File, options?: UploadOptions) => Promise<void>` | Upload function. Options admit `path`, `upsert`, `cacheControl`, `contentType`. |
258
+ | `isUploading` | `boolean` | `true` while the upload is in progress |
259
+ | `progress` | `number` | Upload progress percentage (0–100) updated in real-time |
260
+ | `data` | `{ path: string; fullPath: string } \| null` | Successful upload result |
261
+ | `error` | `SupaError \| null` | Error details if upload failed |
262
+ | `cancel` | `() => void` | Aborts the in-flight upload |
263
+ | `reset` | `() => void` | Resets the hook state |
264
+
265
+ ---
266
+
267
+ #### `useSupaRealtime(table, event, callback, schema?)`
268
+
269
+ React hook that subscribes to Supabase Realtime `postgres_changes` events. **Safely cleans up** the subscription on unmount to prevent memory leaks and duplicate listeners.
270
+
271
+ **Parameters:**
272
+
273
+ - `table` (`string`): The database table to listen to.
274
+ - `event` (`"INSERT" \| "UPDATE" \| "DELETE" \| "*"`): The event type.
275
+ - `callback` (`(payload: RealtimePayload) => void`): Function invoked on each event.
276
+ - `schema` (`string`, default `"public"`): The database schema.
277
+
278
+ **Example:**
279
+
280
+ ```tsx
281
+ useSupaRealtime("messages", "INSERT", (payload) => {
282
+ console.log("New message:", payload.new);
283
+ });
284
+ ```
285
+
286
+ ---
287
+
202
288
  ### Shared — `next-supa-utils`
203
289
 
204
290
  #### `handleSupaError(error)`
@@ -268,12 +354,19 @@ src/
268
354
 
269
355
  | Import | Environment | Contains |
270
356
  |---|---|---|
271
- | `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession` |
272
- | `next-supa-utils/server` | Server Components, Middleware, Server Actions | `withSupaAuth`, `createAction` |
357
+ | `next-supa-utils/client` | Client Components | `useSupaUser`, `useSupaSession`, `useSupaUpload`, `useSupaRealtime` |
358
+ | `next-supa-utils/server` | Server Components, Middleware, Server Actions, Route Handlers | `withSupaAuth`, `createAction`, `routeWrapper` |
273
359
  | `next-supa-utils` | Anywhere | `handleSupaError`, all types |
274
360
 
275
361
  ## Changelog
276
362
 
363
+ ### v0.1.5
364
+
365
+ - **🚀 New Features:**
366
+ - `useSupaUpload` — React hook for uploading files to Supabase Storage with **real-time progress tracking** (using direct XHR to REST API, no extra dependencies) and abort capabilities.
367
+ - `useSupaRealtime` — React hook to subscribe to Supabase Realtime `postgres_changes` events with **safe auto-cleanup** (prevents memory leaks on unmount).
368
+ - `routeWrapper` — Higher-order function for Next.js Route Handlers (API routes) with auto try-catch, standardized error responses, and optional auth gating (`requireAuth: true`).
369
+
277
370
  ### v0.1.3
278
371
 
279
372
  - **🔐 RBAC Support** — `withSupaAuth` now supports **role-based access control**.
@@ -22,7 +22,9 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  // src/client/index.ts
23
23
  var client_exports = {};
24
24
  __export(client_exports, {
25
+ useSupaRealtime: () => useSupaRealtime,
25
26
  useSupaSession: () => useSupaSession,
27
+ useSupaUpload: () => useSupaUpload,
26
28
  useSupaUser: () => useSupaUser
27
29
  });
28
30
  module.exports = __toCommonJS(client_exports);
@@ -146,9 +148,174 @@ function useSupaSession() {
146
148
  }, []);
147
149
  return state;
148
150
  }
151
+
152
+ // src/client/hooks/useSupaUpload.ts
153
+ var import_react3 = require("react");
154
+ var import_ssr3 = require("@supabase/ssr");
155
+ function useSupaUpload(bucketName) {
156
+ const [isUploading, setIsUploading] = (0, import_react3.useState)(false);
157
+ const [progress, setProgress] = (0, import_react3.useState)(0);
158
+ const [data, setData] = (0, import_react3.useState)(null);
159
+ const [error, setError] = (0, import_react3.useState)(null);
160
+ const xhrRef = (0, import_react3.useRef)(null);
161
+ const supabaseRef = (0, import_react3.useRef)(null);
162
+ function getClient() {
163
+ if (supabaseRef.current) return supabaseRef.current;
164
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL;
165
+ const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
166
+ if (!url || !key) {
167
+ throw new Error(
168
+ "Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
169
+ );
170
+ }
171
+ supabaseRef.current = (0, import_ssr3.createBrowserClient)(url, key);
172
+ return supabaseRef.current;
173
+ }
174
+ const upload = (0, import_react3.useCallback)(
175
+ async (file, options) => {
176
+ setIsUploading(true);
177
+ setProgress(0);
178
+ setData(null);
179
+ setError(null);
180
+ try {
181
+ const supabase = getClient();
182
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
183
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
184
+ const {
185
+ data: { session }
186
+ } = await supabase.auth.getSession();
187
+ const accessToken = session?.access_token ?? supabaseAnonKey;
188
+ const filePath = options?.path ?? file.name;
189
+ const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;
190
+ await new Promise((resolve, reject) => {
191
+ const xhr = new XMLHttpRequest();
192
+ xhrRef.current = xhr;
193
+ xhr.upload.addEventListener("progress", (e) => {
194
+ if (e.lengthComputable) {
195
+ const pct = Math.round(e.loaded / e.total * 100);
196
+ setProgress(pct);
197
+ }
198
+ });
199
+ xhr.addEventListener("load", () => {
200
+ xhrRef.current = null;
201
+ if (xhr.status >= 200 && xhr.status < 300) {
202
+ try {
203
+ const response = JSON.parse(xhr.responseText);
204
+ const fullPath = response.Key ?? `${bucketName}/${filePath}`;
205
+ setData({ path: filePath, fullPath });
206
+ setProgress(100);
207
+ resolve();
208
+ } catch {
209
+ setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });
210
+ setProgress(100);
211
+ resolve();
212
+ }
213
+ } else {
214
+ try {
215
+ const errBody = JSON.parse(xhr.responseText);
216
+ reject(
217
+ new Error(
218
+ errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`
219
+ )
220
+ );
221
+ } catch {
222
+ reject(new Error(`Upload failed with status ${xhr.status}`));
223
+ }
224
+ }
225
+ });
226
+ xhr.addEventListener("error", () => {
227
+ xhrRef.current = null;
228
+ reject(new Error("Network error during upload"));
229
+ });
230
+ xhr.addEventListener("abort", () => {
231
+ xhrRef.current = null;
232
+ reject(new Error("Upload cancelled"));
233
+ });
234
+ xhr.open("POST", uploadUrl, true);
235
+ xhr.setRequestHeader("Authorization", `Bearer ${accessToken}`);
236
+ xhr.setRequestHeader("apikey", supabaseAnonKey);
237
+ xhr.setRequestHeader(
238
+ "Content-Type",
239
+ options?.contentType ?? (file.type || "application/octet-stream")
240
+ );
241
+ xhr.setRequestHeader(
242
+ "cache-control",
243
+ options?.cacheControl ?? "3600"
244
+ );
245
+ xhr.setRequestHeader(
246
+ "x-upsert",
247
+ String(options?.upsert ?? false)
248
+ );
249
+ xhr.send(file);
250
+ });
251
+ } catch (caught) {
252
+ setError(handleSupaError(caught));
253
+ setProgress(0);
254
+ } finally {
255
+ setIsUploading(false);
256
+ }
257
+ },
258
+ [bucketName]
259
+ );
260
+ const cancel = (0, import_react3.useCallback)(() => {
261
+ if (xhrRef.current) {
262
+ xhrRef.current.abort();
263
+ xhrRef.current = null;
264
+ }
265
+ }, []);
266
+ const reset = (0, import_react3.useCallback)(() => {
267
+ cancel();
268
+ setIsUploading(false);
269
+ setProgress(0);
270
+ setData(null);
271
+ setError(null);
272
+ }, [cancel]);
273
+ return { upload, isUploading, progress, data, error, reset, cancel };
274
+ }
275
+
276
+ // src/client/hooks/useSupaRealtime.ts
277
+ var import_react4 = require("react");
278
+ var import_ssr4 = require("@supabase/ssr");
279
+ function useSupaRealtime(table, event, callback, schema = "public") {
280
+ const callbackRef = (0, import_react4.useRef)(callback);
281
+ callbackRef.current = callback;
282
+ const channelRef = (0, import_react4.useRef)(null);
283
+ (0, import_react4.useEffect)(() => {
284
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
285
+ const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;
286
+ if (!supabaseUrl || !supabaseAnonKey) {
287
+ console.error(
288
+ "[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables."
289
+ );
290
+ return;
291
+ }
292
+ const supabase = (0, import_ssr4.createBrowserClient)(supabaseUrl, supabaseAnonKey);
293
+ const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;
294
+ const channel = supabase.channel(channelName).on(
295
+ "postgres_changes",
296
+ {
297
+ event: event === "*" ? "*" : event,
298
+ schema,
299
+ table
300
+ },
301
+ (payload) => {
302
+ callbackRef.current(payload);
303
+ }
304
+ ).subscribe();
305
+ channelRef.current = channel;
306
+ return () => {
307
+ if (channelRef.current) {
308
+ supabase.removeChannel(channelRef.current);
309
+ channelRef.current = null;
310
+ }
311
+ };
312
+ }, [table, event, schema]);
313
+ }
149
314
  // Annotate the CommonJS export names for ESM import in node:
150
315
  0 && (module.exports = {
316
+ useSupaRealtime,
151
317
  useSupaSession,
318
+ useSupaUpload,
152
319
  useSupaUser
153
320
  });
154
321
  //# sourceMappingURL=index.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n"],"mappings":"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","names":["import_react","import_ssr"]}
1
+ {"version":3,"sources":["../../src/client/index.ts","../../src/client/hooks/useSupaUser.ts","../../src/shared/utils/error-handler.ts","../../src/client/hooks/useSupaSession.ts","../../src/client/hooks/useSupaUpload.ts","../../src/client/hooks/useSupaRealtime.ts"],"sourcesContent":["\"use client\";\n\n// ── Client entry point ──────────────────────────────────────────────\n// This module MUST only be imported in Client Components.\n// The \"use client\" directive ensures Next.js treats the entire\n// sub-tree as client-side code.\n\nexport { useSupaUser } from \"./hooks/useSupaUser\";\nexport { useSupaSession } from \"./hooks/useSupaSession\";\nexport { useSupaUpload } from \"./hooks/useSupaUpload\";\nexport { useSupaRealtime } from \"./hooks/useSupaRealtime\";\n\n// Re-export types consumers commonly need alongside client helpers.\nexport type {\n UseSupaUserReturn,\n UseSupaSessionReturn,\n UseSupaUploadReturn,\n UploadOptions,\n RealtimeEvent,\n RealtimePayload,\n SupaError,\n} from \"../types\";\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUserReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase user and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUser } from \"next-supa-utils/client\";\n *\n * export default function Avatar() {\n * const { user, loading, error } = useSupaUser();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!user) return <p>Not signed in</p>;\n *\n * return <p>Hello, {user.email}</p>;\n * }\n * ```\n */\nexport function useSupaUser(): UseSupaUserReturn {\n const [state, setState] = useState<UseSupaUserReturn>({\n user: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n user: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getUser().then(({ data, error }) => {\n setState({\n user: data.user,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session) => {\n setState((prev) => ({\n ...prev,\n user: session?.user ?? null,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","import type { SupaError } from \"../../types\";\n\n/**\n * Normalize any thrown value into a consistent `SupaError` shape.\n *\n * Handles:\n * - Supabase `AuthError` / `PostgrestError` (has `.message` and optional `.code` / `.status`)\n * - Standard `Error` instances\n * - Plain strings\n * - Unknown values (fallback)\n */\nexport function handleSupaError(error: unknown): SupaError {\n // ── Supabase errors & standard Error instances ──────────────────\n if (error instanceof Error) {\n const record = error as unknown as Record<string, unknown>;\n return {\n message: error.message,\n code: typeof record.code === \"string\" ? record.code : undefined,\n status: typeof record.status === \"number\" ? record.status : undefined,\n };\n }\n\n // ── Plain object with a message property ────────────────────────\n if (\n typeof error === \"object\" &&\n error !== null &&\n \"message\" in error &&\n typeof (error as Record<string, unknown>).message === \"string\"\n ) {\n const err = error as Record<string, unknown>;\n return {\n message: err.message as string,\n code: typeof err.code === \"string\" ? err.code : undefined,\n status: typeof err.status === \"number\" ? err.status : undefined,\n };\n }\n\n // ── String ──────────────────────────────────────────────────────\n if (typeof error === \"string\") {\n return { message: error };\n }\n\n // ── Fallback ────────────────────────────────────────────────────\n return { message: \"An unknown error occurred\" };\n}\n","\"use client\";\n\nimport { useEffect, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { Session } from \"@supabase/supabase-js\";\n\nimport type { UseSupaSessionReturn } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that provides the current Supabase session and\n * subscribes to real-time auth state changes.\n *\n * Must be used inside a Client Component (`\"use client\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaSession } from \"next-supa-utils/client\";\n *\n * export default function TokenDisplay() {\n * const { session, loading, error } = useSupaSession();\n *\n * if (loading) return <p>Loading…</p>;\n * if (error) return <p>Error: {error.message}</p>;\n * if (!session) return <p>No active session</p>;\n *\n * return <p>Token expires at: {session.expires_at}</p>;\n * }\n * ```\n */\nexport function useSupaSession(): UseSupaSessionReturn {\n const [state, setState] = useState<UseSupaSessionReturn>({\n session: null,\n loading: true,\n error: null,\n });\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n setState({\n session: null,\n loading: false,\n error: {\n message:\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n code: \"CONFIG_ERROR\",\n },\n });\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // ── Initial fetch ─────────────────────────────────────────────\n supabase.auth.getSession().then(({ data, error }) => {\n setState({\n session: data.session,\n loading: false,\n error: error ? handleSupaError(error) : null,\n });\n });\n\n // ── Subscribe to auth state changes ───────────────────────────\n const {\n data: { subscription },\n } = supabase.auth.onAuthStateChange((_event, session: Session | null) => {\n setState((prev) => ({\n ...prev,\n session,\n loading: false,\n }));\n });\n\n return () => {\n subscription.unsubscribe();\n };\n }, []);\n\n return state;\n}\n","\"use client\";\n\nimport { useCallback, useRef, useState } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\n\nimport type { UseSupaUploadReturn, UploadOptions } from \"../../types\";\nimport { handleSupaError } from \"../../shared/utils/error-handler\";\n\n/**\n * React hook that simplifies uploading files to Supabase Storage\n * with **real-time progress tracking**.\n *\n * Uses `XMLHttpRequest` against the Supabase Storage REST API so that\n * `progress` updates smoothly from 0 → 100 as bytes are sent, without\n * requiring `tus-js-client` or any extra dependencies.\n *\n * @param bucketName - The name of the Supabase Storage bucket to upload to.\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaUpload } from \"next-supa-utils/client\";\n *\n * export default function AvatarUploader() {\n * const { upload, isUploading, progress, data, error } = useSupaUpload(\"avatars\");\n *\n * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {\n * const file = e.target.files?.[0];\n * if (!file) return;\n * await upload(file, { path: `users/${file.name}`, upsert: true });\n * };\n *\n * return (\n * <div>\n * <input type=\"file\" onChange={handleChange} disabled={isUploading} />\n * {isUploading && <progress value={progress} max={100} />}\n * {isUploading && <p>{progress}%</p>}\n * {error && <p>Error: {error.message}</p>}\n * {data && <p>Uploaded to: {data.path}</p>}\n * </div>\n * );\n * }\n * ```\n */\nexport function useSupaUpload(bucketName: string): UseSupaUploadReturn {\n const [isUploading, setIsUploading] = useState(false);\n const [progress, setProgress] = useState(0);\n const [data, setData] = useState<UseSupaUploadReturn[\"data\"]>(null);\n const [error, setError] = useState<UseSupaUploadReturn[\"error\"]>(null);\n\n // Abort controller reference so we can cancel in-flight uploads.\n const xhrRef = useRef<XMLHttpRequest | null>(null);\n\n // Stable reference to the Supabase client (used only to get the session token).\n const supabaseRef = useRef<ReturnType<typeof createBrowserClient> | null>(null);\n\n function getClient() {\n if (supabaseRef.current) return supabaseRef.current;\n\n const url = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const key = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!url || !key) {\n throw new Error(\n \"Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n }\n\n supabaseRef.current = createBrowserClient(url, key);\n return supabaseRef.current;\n }\n\n const upload = useCallback(\n async (file: File, options?: UploadOptions) => {\n // ── Reset state ──────────────────────────────────────────────\n setIsUploading(true);\n setProgress(0);\n setData(null);\n setError(null);\n\n try {\n const supabase = getClient();\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;\n\n // Retrieve the current session token for authorization.\n const {\n data: { session },\n } = await supabase.auth.getSession();\n\n const accessToken = session?.access_token ?? supabaseAnonKey;\n const filePath = options?.path ?? file.name;\n\n // ── Build the Storage REST API URL ─────────────────────────\n // POST /storage/v1/object/:bucket/:path\n const uploadUrl = `${supabaseUrl}/storage/v1/object/${bucketName}/${filePath}`;\n\n // ── Upload via XHR for real-time progress ──────────────────\n await new Promise<void>((resolve, reject) => {\n const xhr = new XMLHttpRequest();\n xhrRef.current = xhr;\n\n // ── Progress handler ───────────────────────────────────\n xhr.upload.addEventListener(\"progress\", (e) => {\n if (e.lengthComputable) {\n const pct = Math.round((e.loaded / e.total) * 100);\n setProgress(pct);\n }\n });\n\n // ── Success handler ────────────────────────────────────\n xhr.addEventListener(\"load\", () => {\n xhrRef.current = null;\n\n if (xhr.status >= 200 && xhr.status < 300) {\n try {\n const response = JSON.parse(xhr.responseText) as {\n Key?: string;\n Id?: string;\n };\n // Supabase returns { Key: \"bucket/path\" }\n const fullPath = response.Key ?? `${bucketName}/${filePath}`;\n setData({ path: filePath, fullPath });\n setProgress(100);\n resolve();\n } catch {\n // Response parsed fine even if body structure differs\n setData({ path: filePath, fullPath: `${bucketName}/${filePath}` });\n setProgress(100);\n resolve();\n }\n } else {\n // Server returned an error status\n try {\n const errBody = JSON.parse(xhr.responseText) as {\n statusCode?: string;\n error?: string;\n message?: string;\n };\n reject(\n new Error(\n errBody.message ?? errBody.error ?? `Upload failed with status ${xhr.status}`,\n ),\n );\n } catch {\n reject(new Error(`Upload failed with status ${xhr.status}`));\n }\n }\n });\n\n // ── Error handler ──────────────────────────────────────\n xhr.addEventListener(\"error\", () => {\n xhrRef.current = null;\n reject(new Error(\"Network error during upload\"));\n });\n\n // ── Abort handler ──────────────────────────────────────\n xhr.addEventListener(\"abort\", () => {\n xhrRef.current = null;\n reject(new Error(\"Upload cancelled\"));\n });\n\n // ── Send the request ───────────────────────────────────\n xhr.open(\"POST\", uploadUrl, true);\n xhr.setRequestHeader(\"Authorization\", `Bearer ${accessToken}`);\n xhr.setRequestHeader(\"apikey\", supabaseAnonKey);\n xhr.setRequestHeader(\n \"Content-Type\",\n options?.contentType ?? (file.type || \"application/octet-stream\"),\n );\n xhr.setRequestHeader(\n \"cache-control\",\n options?.cacheControl ?? \"3600\",\n );\n xhr.setRequestHeader(\n \"x-upsert\",\n String(options?.upsert ?? false),\n );\n\n xhr.send(file);\n });\n } catch (caught: unknown) {\n setError(handleSupaError(caught));\n setProgress(0);\n } finally {\n setIsUploading(false);\n }\n },\n [bucketName],\n );\n\n const cancel = useCallback(() => {\n if (xhrRef.current) {\n xhrRef.current.abort();\n xhrRef.current = null;\n }\n }, []);\n\n const reset = useCallback(() => {\n cancel();\n setIsUploading(false);\n setProgress(0);\n setData(null);\n setError(null);\n }, [cancel]);\n\n return { upload, isUploading, progress, data, error, reset, cancel };\n}\n","\"use client\";\n\nimport { useEffect, useRef } from \"react\";\nimport { createBrowserClient } from \"@supabase/ssr\";\nimport type { RealtimeChannel } from \"@supabase/supabase-js\";\n\nimport type { RealtimeEvent, RealtimePayload } from \"../../types\";\n\n/**\n * React hook that subscribes to Supabase Realtime postgres_changes\n * events and **safely cleans up** on unmount to prevent memory leaks.\n *\n * @param table - The database table to listen to.\n * @param event - The event type: `\"INSERT\"`, `\"UPDATE\"`, `\"DELETE\"`, or `\"*\"` for all.\n * @param callback - Function called with the realtime payload on each event.\n * @param schema - The database schema (defaults to `\"public\"`).\n *\n * @example\n * ```tsx\n * \"use client\";\n * import { useSupaRealtime } from \"next-supa-utils/client\";\n *\n * export default function LiveMessages() {\n * const [messages, setMessages] = useState<Message[]>([]);\n *\n * useSupaRealtime(\"messages\", \"INSERT\", (payload) => {\n * setMessages((prev) => [...prev, payload.new as Message]);\n * });\n *\n * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;\n * }\n * ```\n *\n * @example\n * ```tsx\n * // Listen to all events with a custom schema\n * useSupaRealtime(\"orders\", \"*\", (payload) => {\n * console.log(payload.eventType, payload.new, payload.old);\n * }, \"inventory\");\n * ```\n */\nexport function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(\n table: string,\n event: RealtimeEvent,\n callback: (payload: RealtimePayload<T>) => void,\n schema: string = \"public\",\n): void {\n // Store callback in a ref so the channel doesn't need to re-subscribe\n // when only the callback identity changes (common with inline arrows).\n const callbackRef = useRef(callback);\n callbackRef.current = callback;\n\n // Store the channel so we can clean it up.\n const channelRef = useRef<RealtimeChannel | null>(null);\n\n useEffect(() => {\n const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;\n const supabaseAnonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY;\n\n if (!supabaseUrl || !supabaseAnonKey) {\n console.error(\n \"[next-supa-utils] Missing NEXT_PUBLIC_SUPABASE_URL or NEXT_PUBLIC_SUPABASE_ANON_KEY environment variables.\",\n );\n return;\n }\n\n const supabase = createBrowserClient(supabaseUrl, supabaseAnonKey);\n\n // Generate a unique channel name to avoid collisions.\n const channelName = `realtime:${schema}:${table}:${event}:${Date.now()}`;\n\n const channel = supabase\n .channel(channelName)\n .on(\n \"postgres_changes\" as \"postgres_changes\",\n {\n event: event === \"*\" ? \"*\" : event,\n schema,\n table,\n },\n (payload) => {\n callbackRef.current(payload as unknown as RealtimePayload<T>);\n },\n )\n .subscribe();\n\n channelRef.current = channel;\n\n // ── Cleanup: remove the channel on unmount or dep change ──────\n return () => {\n if (channelRef.current) {\n supabase.removeChannel(channelRef.current);\n channelRef.current = null;\n }\n };\n }, [table, event, schema]); // Re-subscribe when table/event/schema changes\n}\n"],"mappings":"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","names":["import_react","import_ssr","import_react","import_ssr","import_react","import_ssr"]}
@@ -16,6 +16,72 @@ interface UseSupaSessionReturn {
16
16
  loading: boolean;
17
17
  error: SupaError | null;
18
18
  }
19
+ /** Options passed to the `upload` function from `useSupaUpload`. */
20
+ interface UploadOptions {
21
+ /**
22
+ * The storage path (including filename) where the file will be saved.
23
+ * If omitted, defaults to `file.name`.
24
+ *
25
+ * @example "avatars/user-123.png"
26
+ */
27
+ path?: string;
28
+ /**
29
+ * Whether to overwrite an existing file at the same path.
30
+ * @default false
31
+ */
32
+ upsert?: boolean;
33
+ /**
34
+ * Cache-Control header value for the uploaded file.
35
+ * @default "3600"
36
+ */
37
+ cacheControl?: string;
38
+ /**
39
+ * MIME type of the file. Defaults to the File's own type.
40
+ */
41
+ contentType?: string;
42
+ }
43
+ /** Return type of the `useSupaUpload` hook. */
44
+ interface UseSupaUploadReturn {
45
+ /** Upload a file to the configured bucket. */
46
+ upload: (file: File, options?: UploadOptions) => Promise<void>;
47
+ /** `true` while an upload is in progress. */
48
+ isUploading: boolean;
49
+ /**
50
+ * Upload progress percentage (0–100).
51
+ * Updates in real-time as bytes are sent via `XMLHttpRequest`.
52
+ */
53
+ progress: number;
54
+ /** Upload result data on success, or `null`. */
55
+ data: {
56
+ path: string;
57
+ fullPath: string;
58
+ } | null;
59
+ /** Error details on failure, or `null`. */
60
+ error: SupaError | null;
61
+ /** Reset all state (isUploading, progress, data, error) to initial values. */
62
+ reset: () => void;
63
+ /** Abort the current in-flight upload. */
64
+ cancel: () => void;
65
+ }
66
+ /** Postgres change event types supported by Supabase Realtime. */
67
+ type RealtimeEvent = "INSERT" | "UPDATE" | "DELETE" | "*";
68
+ /** Payload received from a Supabase Realtime postgres_changes event. */
69
+ interface RealtimePayload<T extends Record<string, unknown> = Record<string, unknown>> {
70
+ /** The type of event that triggered the change. */
71
+ eventType: "INSERT" | "UPDATE" | "DELETE";
72
+ /** The new row data (present on INSERT and UPDATE). */
73
+ new: T;
74
+ /** The old row data (present on UPDATE and DELETE). */
75
+ old: Partial<T>;
76
+ /** The database schema (e.g. "public"). */
77
+ schema: string;
78
+ /** The table name. */
79
+ table: string;
80
+ /** Timestamp of the change. */
81
+ commit_timestamp: string;
82
+ /** Any additional errors from the event. */
83
+ errors: string[] | null;
84
+ }
19
85
 
20
86
  /**
21
87
  * React hook that provides the current Supabase user and
@@ -65,4 +131,77 @@ declare function useSupaUser(): UseSupaUserReturn;
65
131
  */
66
132
  declare function useSupaSession(): UseSupaSessionReturn;
67
133
 
68
- export { type SupaError, type UseSupaSessionReturn, type UseSupaUserReturn, useSupaSession, useSupaUser };
134
+ /**
135
+ * React hook that simplifies uploading files to Supabase Storage
136
+ * with **real-time progress tracking**.
137
+ *
138
+ * Uses `XMLHttpRequest` against the Supabase Storage REST API so that
139
+ * `progress` updates smoothly from 0 → 100 as bytes are sent, without
140
+ * requiring `tus-js-client` or any extra dependencies.
141
+ *
142
+ * @param bucketName - The name of the Supabase Storage bucket to upload to.
143
+ *
144
+ * @example
145
+ * ```tsx
146
+ * "use client";
147
+ * import { useSupaUpload } from "next-supa-utils/client";
148
+ *
149
+ * export default function AvatarUploader() {
150
+ * const { upload, isUploading, progress, data, error } = useSupaUpload("avatars");
151
+ *
152
+ * const handleChange = async (e: React.ChangeEvent<HTMLInputElement>) => {
153
+ * const file = e.target.files?.[0];
154
+ * if (!file) return;
155
+ * await upload(file, { path: `users/${file.name}`, upsert: true });
156
+ * };
157
+ *
158
+ * return (
159
+ * <div>
160
+ * <input type="file" onChange={handleChange} disabled={isUploading} />
161
+ * {isUploading && <progress value={progress} max={100} />}
162
+ * {isUploading && <p>{progress}%</p>}
163
+ * {error && <p>Error: {error.message}</p>}
164
+ * {data && <p>Uploaded to: {data.path}</p>}
165
+ * </div>
166
+ * );
167
+ * }
168
+ * ```
169
+ */
170
+ declare function useSupaUpload(bucketName: string): UseSupaUploadReturn;
171
+
172
+ /**
173
+ * React hook that subscribes to Supabase Realtime postgres_changes
174
+ * events and **safely cleans up** on unmount to prevent memory leaks.
175
+ *
176
+ * @param table - The database table to listen to.
177
+ * @param event - The event type: `"INSERT"`, `"UPDATE"`, `"DELETE"`, or `"*"` for all.
178
+ * @param callback - Function called with the realtime payload on each event.
179
+ * @param schema - The database schema (defaults to `"public"`).
180
+ *
181
+ * @example
182
+ * ```tsx
183
+ * "use client";
184
+ * import { useSupaRealtime } from "next-supa-utils/client";
185
+ *
186
+ * export default function LiveMessages() {
187
+ * const [messages, setMessages] = useState<Message[]>([]);
188
+ *
189
+ * useSupaRealtime("messages", "INSERT", (payload) => {
190
+ * setMessages((prev) => [...prev, payload.new as Message]);
191
+ * });
192
+ *
193
+ * return <ul>{messages.map((m) => <li key={m.id}>{m.text}</li>)}</ul>;
194
+ * }
195
+ * ```
196
+ *
197
+ * @example
198
+ * ```tsx
199
+ * // Listen to all events with a custom schema
200
+ * useSupaRealtime("orders", "*", (payload) => {
201
+ * console.log(payload.eventType, payload.new, payload.old);
202
+ * }, "inventory");
203
+ * ```
204
+ */
205
+ declare function useSupaRealtime<T extends Record<string, unknown> = Record<string, unknown>>(table: string, event: RealtimeEvent, callback: (payload: RealtimePayload<T>) => void, schema?: string): void;
206
+
207
+ export { type RealtimeEvent, type RealtimePayload, type SupaError, type UploadOptions, type UseSupaSessionReturn, type UseSupaUploadReturn, type UseSupaUserReturn, useSupaRealtime, useSupaSession, useSupaUpload, useSupaUser };