Browse Source

feat: implement query-shoprenter Edge Function #89

- Add new query-shoprenter Edge Function for querying ShopRenter data
- Support query_type parameter (order/customer)
- Support filter_by parameter (email/order_id)
- Use internal_api_keys authentication
- Automatic token refresh validation via shoprenter-client
- Permission checking via data_access_permissions
- CORS support for cross-origin requests
- Comprehensive error handling and validation
Claude 5 months ago
parent
commit
637afcd954
1 changed files with 244 additions and 0 deletions
  1. 244 0
      supabase/functions/query-shoprenter/index.ts

+ 244 - 0
supabase/functions/query-shoprenter/index.ts

@@ -0,0 +1,244 @@
+/**
+ * Query ShopRenter Edge Function
+ *
+ * Provides a simple query interface for ShopRenter orders and customers.
+ * Supports filtering by email or order_id with automatic token refresh.
+ *
+ * Authentication: Bearer token using internal_api_keys table
+ *
+ * Query Parameters:
+ * - stores_uuid: UUID of the store (required)
+ * - query_type: "order" or "customer" (required)
+ * - filter_by: "email" or "order_id" (required for orders)
+ * - filter_value: The value to filter by (required when filter_by is set)
+ */
+
+import { serve } from 'https://deno.land/std@0.168.0/http/server.ts';
+import { createClient } from 'https://esm.sh/@supabase/supabase-js@2';
+import {
+  requireInternalApiKey,
+  createInternalApiKeyErrorResponse
+} from '../_shared/internal-api-key-auth.ts';
+import {
+  fetchOrders,
+  fetchCustomers,
+  ShopRenterOrderFilters,
+  ShopRenterCustomerFilters
+} from '../_shared/shoprenter-client.ts';
+
+const corsHeaders = {
+  'Access-Control-Allow-Origin': '*',
+  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
+  'Access-Control-Allow-Methods': 'GET, POST, OPTIONS'
+};
+
+// Initialize Supabase client
+const supabaseUrl = Deno.env.get('SUPABASE_URL')!;
+const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!;
+const supabase = createClient(supabaseUrl, supabaseServiceKey);
+
+serve(async (req: Request) => {
+  // Handle CORS preflight
+  if (req.method === 'OPTIONS') {
+    return new Response(null, { headers: corsHeaders });
+  }
+
+  // Only allow GET requests
+  if (req.method !== 'GET') {
+    return new Response(
+      JSON.stringify({ error: 'Method not allowed. Use GET.' }),
+      {
+        status: 405,
+        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+      }
+    );
+  }
+
+  try {
+    // Authenticate with internal API key
+    const authResult = await requireInternalApiKey(req, supabase, 'read_orders');
+
+    if (!authResult.valid) {
+      return createInternalApiKeyErrorResponse(authResult);
+    }
+
+    // Parse query parameters
+    const url = new URL(req.url);
+    const stores_uuid = url.searchParams.get('stores_uuid');
+    const query_type = url.searchParams.get('query_type');
+    const filter_by = url.searchParams.get('filter_by');
+    const filter_value = url.searchParams.get('filter_value');
+
+    // Validate required parameters
+    if (!stores_uuid) {
+      return new Response(
+        JSON.stringify({ error: 'stores_uuid parameter is required' }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    if (!query_type || !['order', 'customer'].includes(query_type)) {
+      return new Response(
+        JSON.stringify({ error: 'query_type must be either "order" or "customer"' }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    // Validate filter parameters
+    if (filter_by && !filter_value) {
+      return new Response(
+        JSON.stringify({ error: 'filter_value is required when filter_by is specified' }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    // Validate filter_by for each query_type
+    if (query_type === 'order' && filter_by && !['email', 'order_id'].includes(filter_by)) {
+      return new Response(
+        JSON.stringify({ error: 'filter_by for orders must be "email" or "order_id"' }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    if (query_type === 'customer' && filter_by && filter_by !== 'email') {
+      return new Response(
+        JSON.stringify({ error: 'filter_by for customers must be "email"' }),
+        {
+          status: 400,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    // Verify store exists and is ShopRenter
+    const { data: store, error: storeError } = await supabase
+      .from('stores')
+      .select('id, platform_name, store_name, data_access_permissions')
+      .eq('id', stores_uuid)
+      .eq('platform_name', 'shoprenter')
+      .single();
+
+    if (storeError || !store) {
+      return new Response(
+        JSON.stringify({ error: 'ShopRenter store not found' }),
+        {
+          status: 404,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    // Check permissions
+    const permissions = store.data_access_permissions as any;
+
+    if (query_type === 'order' && permissions && !permissions.allow_order_access) {
+      return new Response(
+        JSON.stringify({ error: 'Order access not allowed for this store' }),
+        {
+          status: 403,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    if (query_type === 'customer' && permissions && !permissions.allow_customer_access) {
+      return new Response(
+        JSON.stringify({ error: 'Customer access not allowed for this store' }),
+        {
+          status: 403,
+          headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+        }
+      );
+    }
+
+    // Execute query based on query_type
+    let result: any;
+
+    if (query_type === 'order') {
+      // Build filters for orders
+      const filters: ShopRenterOrderFilters = {};
+
+      if (filter_by === 'email' && filter_value) {
+        filters.email = filter_value;
+      } else if (filter_by === 'order_id' && filter_value) {
+        // Use innerId filter for customer-visible order numbers
+        filters.innerId = filter_value;
+      }
+
+      console.log('[Query ShopRenter] Fetching orders with filters:', filters);
+
+      // Fetch orders from ShopRenter API (token refresh handled automatically)
+      const response = await fetchOrders(stores_uuid, 0, 25, filters);
+      const orders = Array.isArray(response) ? response : (response.items || response.data || response.orders || []);
+
+      result = {
+        success: true,
+        query_type: 'order',
+        filters_applied: {
+          filter_by,
+          filter_value
+        },
+        count: orders.length,
+        data: orders
+      };
+
+    } else if (query_type === 'customer') {
+      // Build filters for customers
+      const filters: ShopRenterCustomerFilters = {};
+
+      if (filter_by === 'email' && filter_value) {
+        filters.email = filter_value;
+      }
+
+      console.log('[Query ShopRenter] Fetching customers with filters:', filters);
+
+      // Fetch customers from ShopRenter API (token refresh handled automatically)
+      const response = await fetchCustomers(stores_uuid, 0, 25, filters);
+      const customers = Array.isArray(response) ? response : (response.items || response.data || response.customers || []);
+
+      result = {
+        success: true,
+        query_type: 'customer',
+        filters_applied: {
+          filter_by,
+          filter_value
+        },
+        count: customers.length,
+        data: customers
+      };
+    }
+
+    return new Response(
+      JSON.stringify(result),
+      {
+        status: 200,
+        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+      }
+    );
+
+  } catch (error: any) {
+    console.error('[Query ShopRenter] Error:', error);
+    return new Response(
+      JSON.stringify({
+        error: 'Internal server error',
+        message: error.message
+      }),
+      {
+        status: 500,
+        headers: { ...corsHeaders, 'Content-Type': 'application/json' }
+      }
+    );
+  }
+});