@pixpilot/supabase-functions-client 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/LICENSE +21 -0
- package/README.md +363 -0
- package/dist/functions-client.d.ts +71 -0
- package/dist/functions-client.js +177 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Pixpilot
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
# @pixpilot/supabase-functions-client
|
|
2
|
+
|
|
3
|
+
A lightweight, type-safe wrapper around the Supabase Edge Functions client with structured error handling, automatic error message extraction, and chainable response helpers.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @pixpilot/supabase-functions-client @supabase/supabase-js
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
```typescript
|
|
18
|
+
import { FunctionsClient } from '@pixpilot/supabase-functions-client';
|
|
19
|
+
import { createClient } from '@supabase/supabase-js';
|
|
20
|
+
|
|
21
|
+
const supabase = createClient('https://your-project.supabase.co', 'your-anon-key');
|
|
22
|
+
|
|
23
|
+
// Wrap the Supabase client once
|
|
24
|
+
const functionsClient = new FunctionsClient(supabase);
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
---
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
### `FunctionsClient`
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
class FunctionsClient {
|
|
35
|
+
constructor(supabaseClient: SupabaseClient);
|
|
36
|
+
|
|
37
|
+
invoke<RequestBody = void, ResponseData = any>(
|
|
38
|
+
functionName: string,
|
|
39
|
+
options?: FunctionInvokeOptions<RequestBody>,
|
|
40
|
+
): Promise<ResponseData>;
|
|
41
|
+
|
|
42
|
+
handleInvoke<T>(
|
|
43
|
+
invokeCall: () => Promise<T>,
|
|
44
|
+
onSuccess: (data: T) => Promise<void> | void,
|
|
45
|
+
onError: (error: EdgeFunctionError) => Promise<void> | void,
|
|
46
|
+
context?: string,
|
|
47
|
+
): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/** @deprecated Use handleInvoke instead. */
|
|
50
|
+
handleResponse<T>(
|
|
51
|
+
result: FunctionsResponse<T>,
|
|
52
|
+
onSuccess: (data: T) => Promise<void> | void,
|
|
53
|
+
onError: (error: string, code?: string, details?: unknown) => Promise<void> | void,
|
|
54
|
+
context?: string,
|
|
55
|
+
): Promise<void>;
|
|
56
|
+
}
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## Usage Patterns
|
|
62
|
+
|
|
63
|
+
### 1. `invoke` – Direct Try/Catch (Recommended)
|
|
64
|
+
|
|
65
|
+
`invoke` either returns data on success or **throws** an `EdgeFunctionError`
|
|
66
|
+
(or `EdgeFunctionNoDataError`) on failure.
|
|
67
|
+
|
|
68
|
+
```typescript
|
|
69
|
+
import {
|
|
70
|
+
EdgeFunctionError,
|
|
71
|
+
EdgeFunctionNoDataError,
|
|
72
|
+
FunctionsClient,
|
|
73
|
+
} from '@pixpilot/supabase-functions-client';
|
|
74
|
+
|
|
75
|
+
interface ProcessJobRequest {
|
|
76
|
+
jobContent: string;
|
|
77
|
+
jobUrl: string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
interface ProcessJobResponse {
|
|
81
|
+
jobId: string;
|
|
82
|
+
status: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
/*
|
|
87
|
+
* RequestBody type is inferred, ResponseData is explicit.
|
|
88
|
+
* Because ProcessJobRequest has required properties, `body` is required
|
|
89
|
+
* in the second argument – TypeScript will enforce this.
|
|
90
|
+
*/
|
|
91
|
+
const job = await functionsClient.invoke<ProcessJobRequest, ProcessJobResponse>(
|
|
92
|
+
'process-job',
|
|
93
|
+
{ body: { jobContent: htmlContent, jobUrl: url } },
|
|
94
|
+
);
|
|
95
|
+
|
|
96
|
+
console.log('Job created:', job.jobId);
|
|
97
|
+
} catch (error) {
|
|
98
|
+
if (error instanceof EdgeFunctionNoDataError) {
|
|
99
|
+
console.error('Function returned no data for:', error.functionName);
|
|
100
|
+
} else if (error instanceof EdgeFunctionError) {
|
|
101
|
+
console.error('Function failed:', error.message);
|
|
102
|
+
if (error.code) console.error('Error code:', error.code);
|
|
103
|
+
if (error.details) console.error('Details:', error.details);
|
|
104
|
+
} else {
|
|
105
|
+
throw error; // Re-throw unexpected errors
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Void body (no request body)
|
|
111
|
+
|
|
112
|
+
When `RequestBody` is `void` or omitted, the options argument is optional and
|
|
113
|
+
`body` is not allowed:
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
interface HealthCheckResponse {
|
|
117
|
+
status: 'ok' | 'degraded';
|
|
118
|
+
timestamp: string;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// No second argument needed
|
|
122
|
+
const health = await functionsClient.invoke<void, HealthCheckResponse>('health-check');
|
|
123
|
+
console.log('Status:', health.status);
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
#### All-optional body
|
|
127
|
+
|
|
128
|
+
When every property of `RequestBody` is optional, the options argument is also
|
|
129
|
+
optional:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
interface SearchRequest {
|
|
133
|
+
query?: string;
|
|
134
|
+
limit?: number;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
interface SearchResponse {
|
|
138
|
+
results: string[];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Can omit options entirely
|
|
142
|
+
const results = await functionsClient.invoke<SearchRequest, SearchResponse>('search');
|
|
143
|
+
|
|
144
|
+
// Or pass a partial body
|
|
145
|
+
const filtered = await functionsClient.invoke<SearchRequest, SearchResponse>('search', {
|
|
146
|
+
body: { query: 'typescript', limit: 10 },
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### 2. `handleInvoke` – Callback Pattern
|
|
153
|
+
|
|
154
|
+
Use `handleInvoke` when you want clean success/error callbacks without a
|
|
155
|
+
try/catch block. Uncaught non-`EdgeFunctionError` exceptions are re-thrown.
|
|
156
|
+
|
|
157
|
+
```typescript
|
|
158
|
+
await functionsClient.handleInvoke(
|
|
159
|
+
// The function call
|
|
160
|
+
() =>
|
|
161
|
+
functionsClient.invoke<ProcessJobRequest, ProcessJobResponse>('process-job', {
|
|
162
|
+
body: { jobContent: htmlContent, jobUrl: url },
|
|
163
|
+
}),
|
|
164
|
+
|
|
165
|
+
// Success handler
|
|
166
|
+
(job) => {
|
|
167
|
+
console.log('Job created:', job.jobId);
|
|
168
|
+
showSuccessNotification(job);
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
// Error handler – receives an EdgeFunctionError
|
|
172
|
+
(error) => {
|
|
173
|
+
console.error('Function failed:', error.message);
|
|
174
|
+
if (error.code === 'QUOTA_EXCEEDED') {
|
|
175
|
+
showQuotaWarning();
|
|
176
|
+
} else {
|
|
177
|
+
showErrorNotification(error.message);
|
|
178
|
+
}
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
// Optional context label – included in log output
|
|
182
|
+
'Process Job',
|
|
183
|
+
);
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### 3. `handleResponse` – Legacy Callback Pattern _(deprecated)_
|
|
189
|
+
|
|
190
|
+
This method takes a `FunctionsResponse` object (the old `{data, error}` shape)
|
|
191
|
+
and dispatches to one of two callbacks. Prefer `handleInvoke` for new code.
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
const result: FunctionsResponse<ProcessJobResponse> = {
|
|
195
|
+
data: { jobId: '123', status: 'pending' },
|
|
196
|
+
error: null,
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
await functionsClient.handleResponse(
|
|
200
|
+
result,
|
|
201
|
+
// onSuccess
|
|
202
|
+
(job) => {
|
|
203
|
+
console.log('Job created:', job.jobId);
|
|
204
|
+
},
|
|
205
|
+
// onError
|
|
206
|
+
(errorMessage, errorCode, details) => {
|
|
207
|
+
console.error('Function failed:', errorMessage);
|
|
208
|
+
if (errorCode) console.error('Error code:', errorCode);
|
|
209
|
+
},
|
|
210
|
+
);
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## Error Classes
|
|
216
|
+
|
|
217
|
+
### `EdgeFunctionError`
|
|
218
|
+
|
|
219
|
+
The base error class for all Edge Function failures.
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
class EdgeFunctionError extends Error {
|
|
223
|
+
/** Name of the Edge Function that failed */
|
|
224
|
+
readonly functionName: string;
|
|
225
|
+
|
|
226
|
+
/** Optional machine-readable error code from the function response */
|
|
227
|
+
readonly code?: string;
|
|
228
|
+
|
|
229
|
+
/** Additional structured details from the function response */
|
|
230
|
+
readonly details?: unknown;
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### `EdgeFunctionNoDataError`
|
|
235
|
+
|
|
236
|
+
Extends `EdgeFunctionError`. Thrown when the function call succeeds (HTTP 2xx)
|
|
237
|
+
but returns no data.
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
class EdgeFunctionNoDataError extends EdgeFunctionError {
|
|
241
|
+
// message: 'No data received from Edge Function.'
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### `instanceof` Checks
|
|
246
|
+
|
|
247
|
+
```typescript
|
|
248
|
+
import {
|
|
249
|
+
EdgeFunctionError,
|
|
250
|
+
EdgeFunctionNoDataError,
|
|
251
|
+
} from '@pixpilot/supabase-functions-client';
|
|
252
|
+
|
|
253
|
+
try {
|
|
254
|
+
const data = await functionsClient.invoke('my-function');
|
|
255
|
+
} catch (error) {
|
|
256
|
+
if (error instanceof EdgeFunctionNoDataError) {
|
|
257
|
+
// Successful HTTP call but empty body
|
|
258
|
+
console.log('No data from:', error.functionName);
|
|
259
|
+
} else if (error instanceof EdgeFunctionError) {
|
|
260
|
+
// HTTP error OR network error
|
|
261
|
+
console.log(`${error.functionName} failed: ${error.message}`);
|
|
262
|
+
if (error.code) console.log('Code:', error.code);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
---
|
|
268
|
+
|
|
269
|
+
## Automatic Error Message Extraction
|
|
270
|
+
|
|
271
|
+
When an Edge Function returns a non-2xx response, the client automatically
|
|
272
|
+
parses the JSON body and extracts the human-readable message. The resolution
|
|
273
|
+
order is:
|
|
274
|
+
|
|
275
|
+
1. `body.error` field
|
|
276
|
+
2. `body.message` field
|
|
277
|
+
3. HTTP error message (fallback)
|
|
278
|
+
|
|
279
|
+
Edge Functions should return a structured error body:
|
|
280
|
+
|
|
281
|
+
```typescript
|
|
282
|
+
// From your Edge Function (Deno / Node):
|
|
283
|
+
return new Response(
|
|
284
|
+
JSON.stringify({
|
|
285
|
+
error: 'User not found', // → EdgeFunctionError.message
|
|
286
|
+
code: 'USER_NOT_FOUND', // → EdgeFunctionError.code
|
|
287
|
+
details: { userId: 123 }, // → EdgeFunctionError.details
|
|
288
|
+
}),
|
|
289
|
+
{ status: 404, headers: { 'Content-Type': 'application/json' } },
|
|
290
|
+
);
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## Nested `ApiResponse` Unwrapping
|
|
296
|
+
|
|
297
|
+
If your Edge Function wraps data in a `{ data, message }` envelope, the client
|
|
298
|
+
automatically unwraps it:
|
|
299
|
+
|
|
300
|
+
```typescript
|
|
301
|
+
// Edge Function returns:
|
|
302
|
+
// { data: { jobId: '123' }, message: 'Success' }
|
|
303
|
+
|
|
304
|
+
// invoke() returns the inner data directly:
|
|
305
|
+
const job = await functionsClient.invoke<void, ProcessJobResponse>('process-job');
|
|
306
|
+
// job === { jobId: '123' }
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
---
|
|
310
|
+
|
|
311
|
+
## Exported Types
|
|
312
|
+
|
|
313
|
+
| Type | Description |
|
|
314
|
+
| ------------------------------------ | ---------------------------------------------------------------------- |
|
|
315
|
+
| `EdgeFunctionErrorResponse` | Shape of the error JSON returned by Edge Functions |
|
|
316
|
+
| `FunctionsResponse<T>` | Legacy `{ data, error, code, details }` response envelope |
|
|
317
|
+
| `FunctionInvokeOptions<RequestBody>` | Options passed to `invoke()`, body requirement adapts to `RequestBody` |
|
|
318
|
+
| `InvokeOptions<RequestBody>` | Determines whether options are required or optional |
|
|
319
|
+
| `RequiredKeys<T>` | Utility: extracts required keys from a type |
|
|
320
|
+
| `HasRequiredProperties<T>` | Utility: `true` if `T` has at least one required key |
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Migration from Direct `supabase.functions.invoke`
|
|
325
|
+
|
|
326
|
+
**Before:**
|
|
327
|
+
|
|
328
|
+
```typescript
|
|
329
|
+
const result = await supabase.functions.invoke<ProcessJobResponse>('process-job', {
|
|
330
|
+
body: { jobContent, jobUrl },
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
if (result.error) {
|
|
334
|
+
if (result.error instanceof FunctionsHttpError) {
|
|
335
|
+
// Manual JSON parsing, error code extraction...
|
|
336
|
+
}
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return result.data ?? undefined;
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**After:**
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
try {
|
|
347
|
+
return await functionsClient.invoke<ProcessJobRequest, ProcessJobResponse>(
|
|
348
|
+
'process-job',
|
|
349
|
+
{ body: { jobContent, jobUrl } },
|
|
350
|
+
);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
if (error instanceof EdgeFunctionError) {
|
|
353
|
+
console.error('Failed:', error.message, error.code);
|
|
354
|
+
}
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
---
|
|
360
|
+
|
|
361
|
+
## License
|
|
362
|
+
|
|
363
|
+
MIT
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { SupabaseClient } from '@supabase/supabase-js';
|
|
2
|
+
import type { FunctionInvokeOptions, FunctionsResponse, RequiredKeys } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Custom error class for Edge Function errors
|
|
5
|
+
* Allows for instanceof checks and provides structured error information
|
|
6
|
+
*/
|
|
7
|
+
export declare class EdgeFunctionError extends Error {
|
|
8
|
+
readonly code?: string | undefined;
|
|
9
|
+
readonly details?: unknown;
|
|
10
|
+
readonly functionName: string;
|
|
11
|
+
constructor(message: string, functionName: string, code?: string, details?: unknown);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Error thrown when Edge Function returns no data
|
|
15
|
+
*/
|
|
16
|
+
export declare class EdgeFunctionNoDataError extends EdgeFunctionError {
|
|
17
|
+
constructor(functionName: string);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Enhanced Supabase Functions client with better error handling
|
|
21
|
+
* Provides a wrapper around Supabase Edge Functions with:
|
|
22
|
+
* - Automatic custom error message extraction
|
|
23
|
+
* - Type-safe responses
|
|
24
|
+
* - Consistent error handling
|
|
25
|
+
* - Generic typing support
|
|
26
|
+
*/
|
|
27
|
+
export declare class FunctionsClient {
|
|
28
|
+
private supabase;
|
|
29
|
+
constructor(supabaseClient: SupabaseClient);
|
|
30
|
+
/**
|
|
31
|
+
* Invoke a Supabase Edge Function with enhanced error handling
|
|
32
|
+
* Throws EdgeFunctionError on failure, returns data on success
|
|
33
|
+
*
|
|
34
|
+
* @param functionName - The name of the Edge Function to invoke
|
|
35
|
+
* @param args - Function invocation options (optional if RequestBody has no required properties)
|
|
36
|
+
* @returns Promise<ResponseData> - The response data on success
|
|
37
|
+
* @throws {EdgeFunctionError} When the function fails or returns an error
|
|
38
|
+
* @throws {EdgeFunctionNoDataError} When the function succeeds but returns no data
|
|
39
|
+
*/
|
|
40
|
+
invoke<RequestBody = void, ResponseData = any>(functionName: string, ...args: RequiredKeys<RequestBody> extends never ? [options?: FunctionInvokeOptions<RequestBody>] : [options: FunctionInvokeOptions<RequestBody>]): Promise<ResponseData>;
|
|
41
|
+
/**
|
|
42
|
+
* Helper method for handling function invocations with UI feedback
|
|
43
|
+
* Use this to wrap invoke calls and handle errors gracefully
|
|
44
|
+
*
|
|
45
|
+
* @param invokeCall - A function that calls invoke and returns the result
|
|
46
|
+
* @param onSuccess - Callback for successful responses
|
|
47
|
+
* @param onError - Callback for error responses
|
|
48
|
+
* @param context - Additional context for logging
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* await functionsClient.handleInvoke(
|
|
53
|
+
* () => functionsClient.invoke<JobData>('process-job', options),
|
|
54
|
+
* (data) => {
|
|
55
|
+
* // Handle success
|
|
56
|
+
* console.log('Job processed:', data);
|
|
57
|
+
* },
|
|
58
|
+
* (error) => {
|
|
59
|
+
* // Handle error
|
|
60
|
+
* showErrorMessage(error.message);
|
|
61
|
+
* }
|
|
62
|
+
* );
|
|
63
|
+
* ```
|
|
64
|
+
*/
|
|
65
|
+
handleInvoke<T>(invokeCall: () => Promise<T>, onSuccess: (data: T) => Promise<void> | void, onError: (error: EdgeFunctionError) => Promise<void> | void, context?: string): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* @deprecated Use handleInvoke instead. This method is kept for backward compatibility.
|
|
68
|
+
* Helper method for handling function responses with UI feedback
|
|
69
|
+
*/
|
|
70
|
+
handleResponse<T>(result: FunctionsResponse<T>, onSuccess: (data: T) => Promise<void> | void, onError: (error: string, code?: string, details?: unknown) => Promise<void> | void, context?: string): Promise<void>;
|
|
71
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { FunctionsHttpError } from '@supabase/supabase-js';
|
|
2
|
+
/**
|
|
3
|
+
* Custom error class for Edge Function errors
|
|
4
|
+
* Allows for instanceof checks and provides structured error information
|
|
5
|
+
*/
|
|
6
|
+
export class EdgeFunctionError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
details;
|
|
9
|
+
functionName;
|
|
10
|
+
constructor(message, functionName, code, details) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'EdgeFunctionError';
|
|
13
|
+
this.functionName = functionName;
|
|
14
|
+
this.code = code;
|
|
15
|
+
this.details = details;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Error thrown when Edge Function returns no data
|
|
20
|
+
*/
|
|
21
|
+
export class EdgeFunctionNoDataError extends EdgeFunctionError {
|
|
22
|
+
constructor(functionName) {
|
|
23
|
+
super('No data received from Edge Function.', functionName);
|
|
24
|
+
this.name = 'EdgeFunctionNoDataError';
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Enhanced Supabase Functions client with better error handling
|
|
29
|
+
* Provides a wrapper around Supabase Edge Functions with:
|
|
30
|
+
* - Automatic custom error message extraction
|
|
31
|
+
* - Type-safe responses
|
|
32
|
+
* - Consistent error handling
|
|
33
|
+
* - Generic typing support
|
|
34
|
+
*/
|
|
35
|
+
export class FunctionsClient {
|
|
36
|
+
supabase;
|
|
37
|
+
constructor(supabaseClient) {
|
|
38
|
+
this.supabase = supabaseClient;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Invoke a Supabase Edge Function with enhanced error handling
|
|
42
|
+
* Throws EdgeFunctionError on failure, returns data on success
|
|
43
|
+
*
|
|
44
|
+
* @param functionName - The name of the Edge Function to invoke
|
|
45
|
+
* @param args - Function invocation options (optional if RequestBody has no required properties)
|
|
46
|
+
* @returns Promise<ResponseData> - The response data on success
|
|
47
|
+
* @throws {EdgeFunctionError} When the function fails or returns an error
|
|
48
|
+
* @throws {EdgeFunctionNoDataError} When the function succeeds but returns no data
|
|
49
|
+
*/
|
|
50
|
+
async invoke(functionName, ...args) {
|
|
51
|
+
const options = args[0];
|
|
52
|
+
try {
|
|
53
|
+
const result = await this.supabase.functions.invoke(functionName,
|
|
54
|
+
// eslint-disable-next-line ts/no-unsafe-argument
|
|
55
|
+
options);
|
|
56
|
+
// Handle error cases
|
|
57
|
+
if (result.error != null) {
|
|
58
|
+
console.error(`Edge Function '${functionName}' error:`, result.error);
|
|
59
|
+
// Handle FunctionsHttpError to extract custom error message
|
|
60
|
+
if (result.error instanceof FunctionsHttpError) {
|
|
61
|
+
try {
|
|
62
|
+
// The context property contains the Response object
|
|
63
|
+
const response = result.error.context;
|
|
64
|
+
const errorResponse = (await response.json());
|
|
65
|
+
const customErrorMessage = errorResponse.error ?? errorResponse.message ?? result.error.message;
|
|
66
|
+
throw new EdgeFunctionError(customErrorMessage, functionName, errorResponse.code, errorResponse.details);
|
|
67
|
+
}
|
|
68
|
+
catch (parseError) {
|
|
69
|
+
if (parseError instanceof EdgeFunctionError) {
|
|
70
|
+
throw parseError;
|
|
71
|
+
}
|
|
72
|
+
console.error(`Failed to parse error response from '${functionName}':`, parseError);
|
|
73
|
+
throw new EdgeFunctionError(result.error.message, functionName);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
// Handle other error types
|
|
78
|
+
const errorMessage = 'message' in result.error
|
|
79
|
+
? result.error.message
|
|
80
|
+
: 'An unknown error occurred.';
|
|
81
|
+
throw new EdgeFunctionError(errorMessage, functionName);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Handle success case
|
|
85
|
+
if (result.data == null) {
|
|
86
|
+
throw new EdgeFunctionNoDataError(functionName);
|
|
87
|
+
}
|
|
88
|
+
// Check if the response follows the ApiResponse structure with nested data
|
|
89
|
+
if (typeof result.data === 'object' &&
|
|
90
|
+
result.data !== null &&
|
|
91
|
+
'data' in result.data &&
|
|
92
|
+
'message' in result.data) {
|
|
93
|
+
// Extract the actual data from the nested structure
|
|
94
|
+
const apiResponse = result.data;
|
|
95
|
+
return apiResponse.data;
|
|
96
|
+
}
|
|
97
|
+
// If it's not nested, return as is
|
|
98
|
+
return result.data;
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
// Re-throw EdgeFunctionError instances
|
|
102
|
+
if (error instanceof EdgeFunctionError) {
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
console.error(`Unexpected error invoking Edge Function '${functionName}':`, error);
|
|
106
|
+
const errorMessage = error instanceof Error
|
|
107
|
+
? error.message
|
|
108
|
+
: `An unexpected error occurred while invoking '${functionName}'.`;
|
|
109
|
+
throw new EdgeFunctionError(errorMessage, functionName);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Helper method for handling function invocations with UI feedback
|
|
114
|
+
* Use this to wrap invoke calls and handle errors gracefully
|
|
115
|
+
*
|
|
116
|
+
* @param invokeCall - A function that calls invoke and returns the result
|
|
117
|
+
* @param onSuccess - Callback for successful responses
|
|
118
|
+
* @param onError - Callback for error responses
|
|
119
|
+
* @param context - Additional context for logging
|
|
120
|
+
*
|
|
121
|
+
* @example
|
|
122
|
+
* ```typescript
|
|
123
|
+
* await functionsClient.handleInvoke(
|
|
124
|
+
* () => functionsClient.invoke<JobData>('process-job', options),
|
|
125
|
+
* (data) => {
|
|
126
|
+
* // Handle success
|
|
127
|
+
* console.log('Job processed:', data);
|
|
128
|
+
* },
|
|
129
|
+
* (error) => {
|
|
130
|
+
* // Handle error
|
|
131
|
+
* showErrorMessage(error.message);
|
|
132
|
+
* }
|
|
133
|
+
* );
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
async handleInvoke(invokeCall, onSuccess, onError, context) {
|
|
137
|
+
try {
|
|
138
|
+
const data = await invokeCall();
|
|
139
|
+
await onSuccess(data);
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof EdgeFunctionError) {
|
|
143
|
+
const contextStr = context != null && context.length > 0 ? ` (${context})` : '';
|
|
144
|
+
console.error(`Function error${contextStr}:`, error.message);
|
|
145
|
+
if (error.code != null && error.code.length > 0) {
|
|
146
|
+
console.error(`Error code${contextStr}:`, error.code);
|
|
147
|
+
}
|
|
148
|
+
await onError(error);
|
|
149
|
+
}
|
|
150
|
+
else {
|
|
151
|
+
// Re-throw unexpected errors
|
|
152
|
+
throw error;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* @deprecated Use handleInvoke instead. This method is kept for backward compatibility.
|
|
158
|
+
* Helper method for handling function responses with UI feedback
|
|
159
|
+
*/
|
|
160
|
+
async handleResponse(result, onSuccess, onError, context) {
|
|
161
|
+
if (result.error != null) {
|
|
162
|
+
const contextStr = context != null && context.length > 0 ? ` (${context})` : '';
|
|
163
|
+
console.error(`Function error${contextStr}:`, result.error);
|
|
164
|
+
if (result.code != null && result.code.length > 0) {
|
|
165
|
+
console.error(`Error code${contextStr}:`, result.code);
|
|
166
|
+
}
|
|
167
|
+
await onError(result.error, result.code, result.details);
|
|
168
|
+
}
|
|
169
|
+
else if (result.data != null) {
|
|
170
|
+
await onSuccess(result.data);
|
|
171
|
+
}
|
|
172
|
+
else {
|
|
173
|
+
// This shouldn't happen with our implementation, but just in case
|
|
174
|
+
await onError('Unexpected response: no data or error received.');
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom error response interface from Edge Functions
|
|
3
|
+
*/
|
|
4
|
+
export interface EdgeFunctionErrorResponse {
|
|
5
|
+
error?: string;
|
|
6
|
+
message?: string;
|
|
7
|
+
code?: string;
|
|
8
|
+
details?: any;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Enhanced function response with better error handling
|
|
12
|
+
*/
|
|
13
|
+
export interface FunctionsResponse<T> {
|
|
14
|
+
data: T | null;
|
|
15
|
+
error: string | null;
|
|
16
|
+
code?: string;
|
|
17
|
+
details?: any;
|
|
18
|
+
message?: string;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Utility to extract required keys from a type
|
|
22
|
+
*/
|
|
23
|
+
export type RequiredKeys<T> = {
|
|
24
|
+
[K in keyof T]-?: Record<string, never> extends {
|
|
25
|
+
[P in K]: T[K];
|
|
26
|
+
} ? never : K;
|
|
27
|
+
}[keyof T];
|
|
28
|
+
/**
|
|
29
|
+
* Check if type has required keys
|
|
30
|
+
*/
|
|
31
|
+
export type HasRequiredProperties<T> = RequiredKeys<T> extends never ? false : true;
|
|
32
|
+
/**
|
|
33
|
+
* Function invocation options with conditional body requirement.
|
|
34
|
+
*
|
|
35
|
+
* - When `RequestBody` is `void | undefined`, the body option is not present.
|
|
36
|
+
* - When `RequestBody` has all-optional properties, `body` is optional.
|
|
37
|
+
* - When `RequestBody` has at least one required property, `body` is required.
|
|
38
|
+
*/
|
|
39
|
+
export type FunctionInvokeOptions<RequestBody> = RequestBody extends void | undefined ? {
|
|
40
|
+
headers?: Record<string, string>;
|
|
41
|
+
method?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE';
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
} : HasRequiredProperties<RequestBody> extends true ? {
|
|
44
|
+
headers?: Record<string, string>;
|
|
45
|
+
method?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE';
|
|
46
|
+
body: RequestBody;
|
|
47
|
+
signal?: AbortSignal;
|
|
48
|
+
} : {
|
|
49
|
+
headers?: Record<string, string>;
|
|
50
|
+
method?: 'POST' | 'GET' | 'PUT' | 'PATCH' | 'DELETE';
|
|
51
|
+
body?: RequestBody;
|
|
52
|
+
signal?: AbortSignal;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Determines whether the `options` parameter to `invoke()` is required or
|
|
56
|
+
* optional based on the shape of `RequestBody`.
|
|
57
|
+
*
|
|
58
|
+
* - If `RequestBody` is `void | undefined`, options are optional.
|
|
59
|
+
* - If `RequestBody` has no required properties, options are optional.
|
|
60
|
+
* - If `RequestBody` has required properties, options are required.
|
|
61
|
+
*/
|
|
62
|
+
export type InvokeOptions<RequestBody> = RequestBody extends void | undefined ? FunctionInvokeOptions<RequestBody> | undefined : RequiredKeys<RequestBody> extends never ? FunctionInvokeOptions<RequestBody> | undefined : FunctionInvokeOptions<RequestBody>;
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@pixpilot/supabase-functions-client",
|
|
3
|
+
"type": "module",
|
|
4
|
+
"version": "0.2.0",
|
|
5
|
+
"description": "A client library for Supabase Functions, providing a simple and efficient way to interact with serverless functions deployed on the Supabase platform.",
|
|
6
|
+
"author": "m.doaie <m.doaie@hotmail.com>",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://github.com/pixpilot/supabase-toolkit",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "https://github.com/pixpilot/supabase-toolkit.git",
|
|
12
|
+
"directory": "packages/supabase-functions-client"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/pixpilot/supabase-toolkit/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js",
|
|
22
|
+
"require": "./dist/index.cjs"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@supabase/supabase-js": "^2.75.0"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@supabase/supabase-js": "^2.75.0",
|
|
36
|
+
"@types/node": "^22.18.10",
|
|
37
|
+
"eslint": "^9.37.0",
|
|
38
|
+
"typescript": "^5.9.3",
|
|
39
|
+
"@internal/eslint-config": "0.3.0",
|
|
40
|
+
"@internal/prettier-config": "0.0.1",
|
|
41
|
+
"@internal/tsdown-config": "0.1.0",
|
|
42
|
+
"@internal/vitest-config": "0.1.0",
|
|
43
|
+
"@internal/tsconfig": "0.1.0"
|
|
44
|
+
},
|
|
45
|
+
"prettier": "@internal/prettier-config",
|
|
46
|
+
"scripts": {
|
|
47
|
+
"clean": "git clean -xdf .cache .turbo dist",
|
|
48
|
+
"clean:all": "pnpm clean && git clean -xdf node_modules",
|
|
49
|
+
"build": "pnpm run clean && tsc -p tsconfig.build.json",
|
|
50
|
+
"build:watch": "pnpm run clean && tsc -p tsconfig.build.json --watch",
|
|
51
|
+
"test": "vitest --run --passWithNoTests",
|
|
52
|
+
"test:watch": "vitest --watch",
|
|
53
|
+
"test:ui": "vitest --ui",
|
|
54
|
+
"test:coverage": "vitest --coverage",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"lint": "eslint",
|
|
57
|
+
"format": "prettier --check . --ignore-path ../../.gitignore --ignore-path ../../.prettierignore"
|
|
58
|
+
},
|
|
59
|
+
"main": "./dist/index.cjs",
|
|
60
|
+
"module": "./dist/index.js",
|
|
61
|
+
"types": "./dist/index.d.ts"
|
|
62
|
+
}
|