needware-cli 1.6.2 → 1.6.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/package.json
CHANGED
|
@@ -123,15 +123,19 @@ Examples:
|
|
|
123
123
|
- `supabase/functions/ai-service/index.ts`
|
|
124
124
|
|
|
125
125
|
```typescript
|
|
126
|
-
import { serve } from "https://deno.land/std@0.
|
|
126
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
127
127
|
|
|
128
|
+
// CORS headers for cross-origin requests
|
|
128
129
|
const corsHeaders = {
|
|
129
130
|
"Access-Control-Allow-Origin": "*",
|
|
130
|
-
"Access-Control-Allow-Headers":
|
|
131
|
+
"Access-Control-Allow-Headers":
|
|
132
|
+
"authorization, x-client-info, apikey, content-type",
|
|
133
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
134
|
+
"Access-Control-Max-Age": "86400",
|
|
131
135
|
};
|
|
132
136
|
|
|
137
|
+
// Request interface
|
|
133
138
|
interface AIRequest {
|
|
134
|
-
// Define request parameters based on specific AI functionality
|
|
135
139
|
input: string | object;
|
|
136
140
|
options?: {
|
|
137
141
|
model?: string;
|
|
@@ -140,10 +144,13 @@ interface AIRequest {
|
|
|
140
144
|
};
|
|
141
145
|
}
|
|
142
146
|
|
|
143
|
-
|
|
144
|
-
// Handle CORS preflight
|
|
147
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
148
|
+
// Handle CORS preflight requests
|
|
145
149
|
if (req.method === "OPTIONS") {
|
|
146
|
-
return new Response(null, {
|
|
150
|
+
return new Response(null, {
|
|
151
|
+
status: 200,
|
|
152
|
+
headers: corsHeaders
|
|
153
|
+
});
|
|
147
154
|
}
|
|
148
155
|
|
|
149
156
|
try {
|
|
@@ -153,27 +160,26 @@ serve(async (req) => {
|
|
|
153
160
|
if (!requestData.input) {
|
|
154
161
|
return new Response(
|
|
155
162
|
JSON.stringify({ error: "Missing required input parameter" }),
|
|
156
|
-
{
|
|
163
|
+
{
|
|
164
|
+
status: 400,
|
|
165
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
166
|
+
}
|
|
157
167
|
);
|
|
158
168
|
}
|
|
159
169
|
|
|
160
|
-
// Get API key
|
|
170
|
+
// Get API key from environment variables
|
|
161
171
|
const AI_API_KEY = Deno.env.get("AI_API_KEY");
|
|
162
172
|
if (!AI_API_KEY) {
|
|
163
|
-
|
|
164
|
-
return new Response(
|
|
165
|
-
JSON.stringify({ error: "AI service not configured" }),
|
|
166
|
-
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
167
|
-
);
|
|
173
|
+
throw new Error("AI service not configured");
|
|
168
174
|
}
|
|
169
175
|
|
|
170
176
|
console.log("Processing AI request...");
|
|
171
177
|
|
|
172
|
-
// Call AI
|
|
178
|
+
// Call AI Gateway API
|
|
173
179
|
const response = await fetch("https://ai.gateway.needware.dev/v1/chat/completions", {
|
|
174
180
|
method: "POST",
|
|
175
181
|
headers: {
|
|
176
|
-
Authorization: `Bearer ${AI_API_KEY}`,
|
|
182
|
+
"Authorization": `Bearer ${AI_API_KEY}`,
|
|
177
183
|
"Content-Type": "application/json",
|
|
178
184
|
},
|
|
179
185
|
body: JSON.stringify({
|
|
@@ -185,7 +191,7 @@ serve(async (req) => {
|
|
|
185
191
|
},
|
|
186
192
|
{
|
|
187
193
|
role: "user",
|
|
188
|
-
content: typeof requestData.input ===
|
|
194
|
+
content: typeof requestData.input === "string"
|
|
189
195
|
? requestData.input
|
|
190
196
|
: JSON.stringify(requestData.input)
|
|
191
197
|
}
|
|
@@ -195,7 +201,7 @@ serve(async (req) => {
|
|
|
195
201
|
}),
|
|
196
202
|
});
|
|
197
203
|
|
|
198
|
-
// Handle error
|
|
204
|
+
// Handle error responses
|
|
199
205
|
if (!response.ok) {
|
|
200
206
|
const errorText = await response.text();
|
|
201
207
|
console.error("AI service error:", response.status, errorText);
|
|
@@ -203,20 +209,24 @@ serve(async (req) => {
|
|
|
203
209
|
if (response.status === 429) {
|
|
204
210
|
return new Response(
|
|
205
211
|
JSON.stringify({ error: "Request rate too high, please try again later" }),
|
|
206
|
-
{
|
|
212
|
+
{
|
|
213
|
+
status: 429,
|
|
214
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
215
|
+
}
|
|
207
216
|
);
|
|
208
217
|
}
|
|
218
|
+
|
|
209
219
|
if (response.status === 402) {
|
|
210
220
|
return new Response(
|
|
211
221
|
JSON.stringify({ error: "AI service quota exhausted" }),
|
|
212
|
-
{
|
|
222
|
+
{
|
|
223
|
+
status: 402,
|
|
224
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
225
|
+
}
|
|
213
226
|
);
|
|
214
227
|
}
|
|
215
228
|
|
|
216
|
-
|
|
217
|
-
JSON.stringify({ error: "AI processing failed" }),
|
|
218
|
-
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
219
|
-
);
|
|
229
|
+
throw new Error(`AI processing failed: ${errorText}`);
|
|
220
230
|
}
|
|
221
231
|
|
|
222
232
|
// Parse AI response
|
|
@@ -224,32 +234,36 @@ serve(async (req) => {
|
|
|
224
234
|
const content = data.choices?.[0]?.message?.content;
|
|
225
235
|
|
|
226
236
|
if (!content) {
|
|
227
|
-
|
|
228
|
-
return new Response(
|
|
229
|
-
JSON.stringify({ error: "No AI response generated" }),
|
|
230
|
-
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
231
|
-
);
|
|
237
|
+
throw new Error("No AI response generated");
|
|
232
238
|
}
|
|
233
239
|
|
|
234
240
|
console.log("AI processing completed");
|
|
235
241
|
|
|
236
|
-
// Return processing result
|
|
237
242
|
return new Response(
|
|
238
243
|
JSON.stringify({
|
|
244
|
+
success: true,
|
|
239
245
|
result: content,
|
|
240
246
|
model: requestData.options?.model || "google/gemini-2.5-flash"
|
|
241
247
|
}),
|
|
242
|
-
{
|
|
248
|
+
{
|
|
249
|
+
status: 200,
|
|
250
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
251
|
+
}
|
|
243
252
|
);
|
|
244
253
|
|
|
245
|
-
} catch (error) {
|
|
254
|
+
} catch (error: any) {
|
|
246
255
|
console.error("Function execution error:", error);
|
|
247
256
|
return new Response(
|
|
248
|
-
JSON.stringify({ error: error
|
|
249
|
-
{
|
|
257
|
+
JSON.stringify({ error: error.message || "Unknown error" }),
|
|
258
|
+
{
|
|
259
|
+
status: 500,
|
|
260
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
261
|
+
}
|
|
250
262
|
);
|
|
251
263
|
}
|
|
252
|
-
}
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
serve(handler);
|
|
253
267
|
```
|
|
254
268
|
|
|
255
269
|
### Frontend Usage Template (TypeScript)
|
|
@@ -515,42 +529,59 @@ Use this template when users need image analysis functionality.
|
|
|
515
529
|
|
|
516
530
|
```typescript
|
|
517
531
|
// supabase/functions/analyze-image/index.ts
|
|
518
|
-
import { serve } from "https://deno.land/std@0.
|
|
532
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
519
533
|
|
|
534
|
+
// CORS headers for cross-origin requests
|
|
520
535
|
const corsHeaders = {
|
|
521
536
|
"Access-Control-Allow-Origin": "*",
|
|
522
|
-
"Access-Control-Allow-Headers":
|
|
537
|
+
"Access-Control-Allow-Headers":
|
|
538
|
+
"authorization, x-client-info, apikey, content-type",
|
|
539
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
540
|
+
"Access-Control-Max-Age": "86400",
|
|
523
541
|
};
|
|
524
542
|
|
|
525
|
-
|
|
543
|
+
// Request interface
|
|
544
|
+
interface ImageAnalysisRequest {
|
|
545
|
+
image: string;
|
|
546
|
+
prompt?: string;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
550
|
+
// Handle CORS preflight requests
|
|
526
551
|
if (req.method === "OPTIONS") {
|
|
527
|
-
return new Response(null, {
|
|
552
|
+
return new Response(null, {
|
|
553
|
+
status: 200,
|
|
554
|
+
headers: corsHeaders
|
|
555
|
+
});
|
|
528
556
|
}
|
|
529
557
|
|
|
530
558
|
try {
|
|
531
|
-
const { image, prompt } = await req.json();
|
|
559
|
+
const { image, prompt }: ImageAnalysisRequest = await req.json();
|
|
532
560
|
|
|
561
|
+
// Validate input
|
|
533
562
|
if (!image) {
|
|
534
563
|
return new Response(
|
|
535
564
|
JSON.stringify({ error: "No image provided" }),
|
|
536
|
-
{
|
|
565
|
+
{
|
|
566
|
+
status: 400,
|
|
567
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
568
|
+
}
|
|
537
569
|
);
|
|
538
570
|
}
|
|
539
571
|
|
|
572
|
+
// Get AI API key from environment
|
|
540
573
|
const AI_API_KEY = Deno.env.get("AI_API_KEY");
|
|
541
574
|
if (!AI_API_KEY) {
|
|
542
|
-
|
|
543
|
-
JSON.stringify({ error: "AI service not configured" }),
|
|
544
|
-
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
545
|
-
);
|
|
575
|
+
throw new Error("AI service not configured");
|
|
546
576
|
}
|
|
547
577
|
|
|
548
578
|
console.log("Analyzing image...");
|
|
549
579
|
|
|
580
|
+
// Call AI Gateway API
|
|
550
581
|
const response = await fetch("https://ai.gateway.needware.dev/v1/chat/completions", {
|
|
551
582
|
method: "POST",
|
|
552
583
|
headers: {
|
|
553
|
-
Authorization: `Bearer ${AI_API_KEY}`,
|
|
584
|
+
"Authorization": `Bearer ${AI_API_KEY}`,
|
|
554
585
|
"Content-Type": "application/json",
|
|
555
586
|
},
|
|
556
587
|
body: JSON.stringify({
|
|
@@ -582,16 +613,13 @@ serve(async (req) => {
|
|
|
582
613
|
if (!response.ok) {
|
|
583
614
|
const errorText = await response.text();
|
|
584
615
|
console.error("AI service error:", response.status, errorText);
|
|
585
|
-
|
|
586
|
-
JSON.stringify({ error: "Image analysis failed" }),
|
|
587
|
-
{ status: response.status, headers: { ...corsHeaders, "Content-Type": "application/json" } }
|
|
588
|
-
);
|
|
616
|
+
throw new Error(`Image analysis failed: ${errorText}`);
|
|
589
617
|
}
|
|
590
618
|
|
|
591
619
|
const data = await response.json();
|
|
592
620
|
const analysis = data.choices?.[0]?.message?.content;
|
|
593
621
|
|
|
594
|
-
//
|
|
622
|
+
// Try to parse structured JSON from response
|
|
595
623
|
let structuredResult;
|
|
596
624
|
try {
|
|
597
625
|
const jsonMatch = analysis.match(/```json\n?([\s\S]*?)\n?```/) || analysis.match(/\{[\s\S]*\}/);
|
|
@@ -608,20 +636,29 @@ serve(async (req) => {
|
|
|
608
636
|
|
|
609
637
|
return new Response(
|
|
610
638
|
JSON.stringify({
|
|
639
|
+
success: true,
|
|
611
640
|
result: structuredResult || analysis,
|
|
612
641
|
model: "google/gemini-2.5-flash"
|
|
613
642
|
}),
|
|
614
|
-
{
|
|
643
|
+
{
|
|
644
|
+
status: 200,
|
|
645
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
646
|
+
}
|
|
615
647
|
);
|
|
616
648
|
|
|
617
|
-
} catch (error) {
|
|
649
|
+
} catch (error: any) {
|
|
618
650
|
console.error("Image analysis error:", error);
|
|
619
651
|
return new Response(
|
|
620
|
-
JSON.stringify({ error: error
|
|
621
|
-
{
|
|
652
|
+
JSON.stringify({ error: error.message || "Unknown error" }),
|
|
653
|
+
{
|
|
654
|
+
status: 500,
|
|
655
|
+
headers: { "Content-Type": "application/json", ...corsHeaders }
|
|
656
|
+
}
|
|
622
657
|
);
|
|
623
658
|
}
|
|
624
|
-
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
serve(handler);
|
|
625
662
|
```
|
|
626
663
|
|
|
627
664
|
## Implementation Checklist
|
|
@@ -120,8 +120,11 @@ const corsHeaders = {
|
|
|
120
120
|
"Access-Control-Allow-Origin": "*",
|
|
121
121
|
"Access-Control-Allow-Headers":
|
|
122
122
|
"authorization, x-client-info, apikey, content-type",
|
|
123
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
124
|
+
"Access-Control-Max-Age": "86400",
|
|
123
125
|
};
|
|
124
126
|
|
|
127
|
+
|
|
125
128
|
interface EmailRequest {
|
|
126
129
|
to: string;
|
|
127
130
|
subject: string;
|
|
@@ -131,7 +134,10 @@ interface EmailRequest {
|
|
|
131
134
|
const handler = async (req: Request): Promise<Response> => {
|
|
132
135
|
// Handle CORS preflight requests
|
|
133
136
|
if (req.method === "OPTIONS") {
|
|
134
|
-
return new Response(null, {
|
|
137
|
+
return new Response(null, {
|
|
138
|
+
status: 200,
|
|
139
|
+
headers: corsHeaders
|
|
140
|
+
});
|
|
135
141
|
}
|
|
136
142
|
|
|
137
143
|
try {
|
|
@@ -186,7 +192,10 @@ interface BatchEmailRequest {
|
|
|
186
192
|
|
|
187
193
|
const handler = async (req: Request): Promise<Response> => {
|
|
188
194
|
if (req.method === "OPTIONS") {
|
|
189
|
-
return new Response(null, {
|
|
195
|
+
return new Response(null, {
|
|
196
|
+
status: 200,
|
|
197
|
+
headers: corsHeaders
|
|
198
|
+
});
|
|
190
199
|
}
|
|
191
200
|
|
|
192
201
|
try {
|
|
@@ -251,6 +260,8 @@ const corsHeaders = {
|
|
|
251
260
|
"Access-Control-Allow-Origin": "*",
|
|
252
261
|
"Access-Control-Allow-Headers":
|
|
253
262
|
"authorization, x-client-info, apikey, content-type",
|
|
263
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
264
|
+
"Access-Control-Max-Age": "86400",
|
|
254
265
|
};
|
|
255
266
|
|
|
256
267
|
interface AlertEmailRequest {
|
|
@@ -341,48 +341,87 @@ Create functions in `/supabase/functions/<function-name>/index.ts`
|
|
|
341
341
|
- **Runtime**: Deno with TypeScript support
|
|
342
342
|
- **Import Supabase**: `import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'`
|
|
343
343
|
- **Environment Variables**: `Deno.env.get('VARIABLE_NAME')`
|
|
344
|
+
- **Standard Library**: Use Deno standard library version 0.190.0 or higher
|
|
345
|
+
|
|
346
|
+
### Edge Function Best Practices
|
|
347
|
+
|
|
348
|
+
- ✅ Always include CORS headers for cross-origin requests
|
|
349
|
+
- ✅ Handle OPTIONS requests for CORS preflight
|
|
350
|
+
- ✅ Define TypeScript interfaces for request/response types
|
|
351
|
+
- ✅ Use try-catch for comprehensive error handling
|
|
352
|
+
- ✅ Log errors with `console.error()` for debugging
|
|
353
|
+
- ✅ Return consistent response format with status codes
|
|
354
|
+
- ✅ Validate input data before processing
|
|
355
|
+
- ✅ Use environment variables for sensitive data
|
|
344
356
|
|
|
345
357
|
### Edge Function Example
|
|
346
358
|
|
|
347
359
|
```typescript
|
|
348
|
-
import { serve } from
|
|
349
|
-
import { createClient } from
|
|
360
|
+
import { serve } from "https://deno.land/std@0.190.0/http/server.ts";
|
|
361
|
+
import { createClient } from "https://esm.sh/@supabase/supabase-js@2";
|
|
362
|
+
|
|
363
|
+
// CORS headers for cross-origin requests
|
|
364
|
+
const corsHeaders = {
|
|
365
|
+
"Access-Control-Allow-Origin": "*",
|
|
366
|
+
"Access-Control-Allow-Headers":
|
|
367
|
+
"authorization, x-client-info, apikey, content-type",
|
|
368
|
+
"Access-Control-Allow-Methods": "POST, OPTIONS",
|
|
369
|
+
"Access-Control-Max-Age": "86400",
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// Request interface
|
|
373
|
+
interface UserQueryRequest {
|
|
374
|
+
name: string;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const handler = async (req: Request): Promise<Response> => {
|
|
378
|
+
// Handle CORS preflight requests
|
|
379
|
+
if (req.method === "OPTIONS") {
|
|
380
|
+
return new Response(null, {
|
|
381
|
+
status: 200,
|
|
382
|
+
headers: corsHeaders
|
|
383
|
+
});
|
|
384
|
+
}
|
|
350
385
|
|
|
351
|
-
serve(async (req) => {
|
|
352
386
|
try {
|
|
353
|
-
const { name } = await req.json();
|
|
354
|
-
|
|
387
|
+
const { name }: UserQueryRequest = await req.json();
|
|
388
|
+
|
|
355
389
|
// Create Supabase client
|
|
356
390
|
const supabaseClient = createClient(
|
|
357
|
-
Deno.env.get(
|
|
358
|
-
Deno.env.get(
|
|
391
|
+
Deno.env.get("SUPABASE_URL") ?? "",
|
|
392
|
+
Deno.env.get("SUPABASE_ANON_KEY") ?? ""
|
|
359
393
|
);
|
|
360
|
-
|
|
394
|
+
|
|
361
395
|
// Execute business logic
|
|
362
396
|
const { data, error } = await supabaseClient
|
|
363
|
-
.from(
|
|
364
|
-
.select(
|
|
365
|
-
.eq(
|
|
366
|
-
|
|
367
|
-
if (error)
|
|
368
|
-
|
|
397
|
+
.from("users")
|
|
398
|
+
.select("*")
|
|
399
|
+
.eq("name", name);
|
|
400
|
+
|
|
401
|
+
if (error) {
|
|
402
|
+
throw new Error(error.message);
|
|
403
|
+
}
|
|
404
|
+
|
|
369
405
|
return new Response(
|
|
370
|
-
JSON.stringify({ data }),
|
|
371
|
-
{
|
|
372
|
-
|
|
373
|
-
|
|
406
|
+
JSON.stringify({ success: true, data }),
|
|
407
|
+
{
|
|
408
|
+
status: 200,
|
|
409
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
374
410
|
}
|
|
375
411
|
);
|
|
376
|
-
} catch (error) {
|
|
412
|
+
} catch (error: any) {
|
|
413
|
+
console.error("Error querying users:", error);
|
|
377
414
|
return new Response(
|
|
378
415
|
JSON.stringify({ error: error.message }),
|
|
379
|
-
{
|
|
380
|
-
|
|
381
|
-
|
|
416
|
+
{
|
|
417
|
+
status: 500,
|
|
418
|
+
headers: { "Content-Type": "application/json", ...corsHeaders },
|
|
382
419
|
}
|
|
383
420
|
);
|
|
384
421
|
}
|
|
385
|
-
}
|
|
422
|
+
};
|
|
423
|
+
|
|
424
|
+
serve(handler);
|
|
386
425
|
```
|
|
387
426
|
|
|
388
427
|
### Invoking from Client
|
|
@@ -391,16 +430,38 @@ serve(async (req) => {
|
|
|
391
430
|
const { data, error } = await supabase.functions.invoke('function-name', {
|
|
392
431
|
body: { name: 'John' }
|
|
393
432
|
});
|
|
433
|
+
|
|
434
|
+
if (error) {
|
|
435
|
+
console.error('Function invocation error:', error);
|
|
436
|
+
} else {
|
|
437
|
+
console.log('Response:', data);
|
|
438
|
+
}
|
|
394
439
|
```
|
|
395
440
|
|
|
396
441
|
### Common Use Cases
|
|
397
442
|
|
|
398
|
-
- Webhook handlers
|
|
399
|
-
- Scheduled jobs
|
|
400
|
-
- Third-party API calls
|
|
401
|
-
- Complex business logic
|
|
402
|
-
- Payment processing
|
|
403
|
-
- Email sending
|
|
443
|
+
- **Webhook handlers**: Process incoming webhooks from third-party services
|
|
444
|
+
- **Scheduled jobs**: Execute periodic tasks (with cron triggers)
|
|
445
|
+
- **Third-party API calls**: Integrate with external services
|
|
446
|
+
- **Complex business logic**: Server-side computations and validations
|
|
447
|
+
- **Payment processing**: Handle payment transactions securely
|
|
448
|
+
- **Email sending**: Send transactional emails (e.g., with Resend)
|
|
449
|
+
- **Authentication flows**: Custom authentication logic
|
|
450
|
+
- **Data transformations**: Process and transform data before storage
|
|
451
|
+
|
|
452
|
+
### Environment Variables in Edge Functions
|
|
453
|
+
|
|
454
|
+
Store sensitive data in environment variables (managed via Needware `add-secret` tool):
|
|
455
|
+
|
|
456
|
+
```typescript
|
|
457
|
+
// Common environment variables
|
|
458
|
+
const SUPABASE_URL = Deno.env.get("SUPABASE_URL");
|
|
459
|
+
const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY");
|
|
460
|
+
const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
461
|
+
const CUSTOM_API_KEY = Deno.env.get("CUSTOM_API_KEY");
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**Note**: Use `add-secret` tool to securely add environment variables to your Edge Functions.
|
|
404
465
|
|
|
405
466
|
## Security
|
|
406
467
|
|