Browse Source

feat: add knowledge data management API endpoints #44

Claude 5 months ago
parent
commit
ac3451d5bd
1 changed files with 444 additions and 0 deletions
  1. 444 0
      supabase/functions/api/index.ts

+ 444 - 0
supabase/functions/api/index.ts

@@ -467,6 +467,450 @@ serve(async (req) => {
       )
     }
 
+    // GET /api/stores/:id/knowledge-data - List knowledge data items
+    if (path.match(/^stores\/[^\/]+\/knowledge-data$/) && req.method === 'GET') {
+      const storeId = path.split('/')[1]
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get query parameters
+      const dataType = url.searchParams.get('data_type')
+      const search = url.searchParams.get('search')
+      const page = parseInt(url.searchParams.get('page') || '1')
+      const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 100)
+      const offset = (page - 1) * limit
+
+      try {
+        // Build queries based on platform and data type
+        const results: any[] = []
+        const platform = store.platform_name
+
+        // Helper to get table name and ID column
+        const getTableInfo = (type: string) => {
+          if (platform === 'woocommerce') {
+            return { table: `woocommerce_${type}s_cache`, idCol: `wc_${type}_id` }
+          } else if (platform === 'shopify') {
+            return { table: `shopify_${type}s_cache`, idCol: `shopify_${type}_id` }
+          } else if (platform === 'shoprenter') {
+            return { table: `shoprenter_${type}s_cache`, idCol: `shoprenter_${type}_id` }
+          }
+          return null
+        }
+
+        // Determine which data types to query
+        const typesToQuery = dataType ? [dataType] : ['product', 'order', 'customer']
+
+        for (const type of typesToQuery) {
+          const tableInfo = getTableInfo(type)
+          if (!tableInfo) continue
+
+          // First, fetch all cache items
+          let cacheQuery = supabase
+            .from(tableInfo.table)
+            .select('*')
+            .eq('store_id', storeId)
+            .order('created_at', { ascending: false })
+
+          // Apply search filter
+          if (search) {
+            if (type === 'product') {
+              if (platform === 'shoprenter') {
+                // For ShopRenter, search in product_data jsonb
+                cacheQuery = cacheQuery.or(`product_data->>name.ilike.%${search}%,product_data->>sku.ilike.%${search}%`)
+              } else {
+                cacheQuery = cacheQuery.or(`name.ilike.%${search}%,sku.ilike.%${search}%`)
+              }
+            } else if (type === 'order') {
+              cacheQuery = cacheQuery.or(`order_number.ilike.%${search}%,customer_name.ilike.%${search}%,customer_email.ilike.%${search}%`)
+            } else if (type === 'customer') {
+              cacheQuery = cacheQuery.or(`email.ilike.%${search}%,first_name.ilike.%${search}%,last_name.ilike.%${search}%,phone.ilike.%${search}%`)
+            }
+          }
+
+          const { data: cacheItems, error: cacheError } = await cacheQuery
+
+          if (cacheError) {
+            console.error(`Error fetching ${type}s from cache:`, cacheError)
+            continue
+          }
+
+          if (!cacheItems || cacheItems.length === 0) continue
+
+          // Get all exclusions for this store and type
+          const { data: exclusions } = await supabase
+            .from('store_data_exclusions')
+            .select('data_id, is_enabled, metadata')
+            .eq('store_id', storeId)
+            .eq('data_type', type)
+
+          // Create a map of exclusions for quick lookup
+          const exclusionMap = new Map()
+          if (exclusions) {
+            exclusions.forEach(e => {
+              exclusionMap.set(e.data_id, { is_enabled: e.is_enabled, metadata: e.metadata })
+            })
+          }
+
+          // Transform cache items to result format
+          for (const item of cacheItems) {
+            const itemId = item[tableInfo.idCol]
+            const exclusionData = exclusionMap.get(itemId)
+
+            let resultItem: any = {
+              data_type: type,
+              data_id: itemId,
+              is_enabled: exclusionData ? exclusionData.is_enabled : true
+            }
+
+            // Add type-specific fields
+            if (type === 'product') {
+              if (platform === 'shoprenter') {
+                resultItem.name = item.product_data?.name
+                resultItem.sku = item.product_data?.sku
+                resultItem.price = item.product_data?.price
+              } else {
+                resultItem.name = item.name
+                resultItem.sku = item.sku
+                resultItem.price = item.price
+              }
+            } else if (type === 'order') {
+              resultItem.order_number = item.order_number
+              resultItem.customer_name = item.customer_name
+              resultItem.customer_email = item.customer_email
+              resultItem.total = item.total
+            } else if (type === 'customer') {
+              resultItem.email = item.email
+              resultItem.first_name = item.first_name
+              resultItem.last_name = item.last_name
+              resultItem.phone = item.phone || ''
+            }
+
+            results.push(resultItem)
+          }
+        }
+
+        // Apply pagination to combined results
+        const totalCount = results.length
+        const paginatedResults = results.slice(offset, offset + limit)
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            data: paginatedResults,
+            pagination: {
+              page,
+              limit,
+              total: totalCount,
+              totalPages: Math.ceil(totalCount / limit)
+            }
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error fetching knowledge data:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to fetch knowledge data' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // PUT /api/stores/:id/knowledge-data/:dataType/:dataId - Toggle item state
+    if (path.match(/^stores\/[^\/]+\/knowledge-data\/[^\/]+\/[^\/]+$/) && req.method === 'PUT') {
+      const pathParts = path.split('/')
+      const storeId = pathParts[1]
+      const dataType = pathParts[3]
+      const dataId = pathParts[4]
+
+      // Validate data_type
+      if (!['product', 'order', 'customer'].includes(dataType)) {
+        return new Response(
+          JSON.stringify({ error: 'Invalid data_type. Must be product, order, or customer' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get request body
+      const body = await req.json()
+      const isEnabled = body.is_enabled !== undefined ? body.is_enabled : true
+
+      try {
+        // Extract metadata from cache tables
+        const platform = store.platform_name
+        const tableName = platform === 'woocommerce'
+          ? `woocommerce_${dataType}s_cache`
+          : platform === 'shopify'
+          ? `shopify_${dataType}s_cache`
+          : `shoprenter_${dataType}s_cache`
+
+        const idColumn = platform === 'woocommerce'
+          ? `wc_${dataType}_id`
+          : platform === 'shopify'
+          ? `shopify_${dataType}_id`
+          : `shoprenter_${dataType}_id`
+
+        // Fetch item from cache to extract metadata
+        let metadata = {}
+        const { data: cacheItem } = await supabase
+          .from(tableName)
+          .select('*')
+          .eq('store_id', storeId)
+          .eq(idColumn, dataId)
+          .single()
+
+        if (cacheItem) {
+          if (dataType === 'product') {
+            if (platform === 'shoprenter') {
+              metadata = {
+                name: cacheItem.product_data?.name,
+                sku: cacheItem.product_data?.sku,
+                price: cacheItem.product_data?.price
+              }
+            } else {
+              metadata = {
+                name: cacheItem.name,
+                sku: cacheItem.sku,
+                price: cacheItem.price
+              }
+            }
+          } else if (dataType === 'order') {
+            metadata = {
+              order_number: cacheItem.order_number,
+              customer_name: cacheItem.customer_name,
+              customer_email: cacheItem.customer_email,
+              total: cacheItem.total
+            }
+          } else if (dataType === 'customer') {
+            metadata = {
+              email: cacheItem.email,
+              first_name: cacheItem.first_name,
+              last_name: cacheItem.last_name,
+              phone: cacheItem.phone
+            }
+          }
+        }
+
+        // Upsert into store_data_exclusions
+        const { data: exclusion, error: upsertError } = await supabase
+          .from('store_data_exclusions')
+          .upsert({
+            store_id: storeId,
+            data_type: dataType,
+            data_id: dataId,
+            is_enabled: isEnabled,
+            metadata,
+            updated_at: new Date().toISOString()
+          }, {
+            onConflict: 'store_id,data_type,data_id'
+          })
+          .select()
+          .single()
+
+        if (upsertError) {
+          console.error('Error upserting exclusion:', upsertError)
+          return new Response(
+            JSON.stringify({ error: 'Failed to update item state' }),
+            { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+          )
+        }
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Item ${isEnabled ? 'enabled' : 'disabled'} successfully`,
+            data: exclusion
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error updating item state:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to update item state' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
+    // POST /api/stores/:id/knowledge-data/batch - Bulk enable/disable items
+    if (path.match(/^stores\/[^\/]+\/knowledge-data\/batch$/) && req.method === 'POST') {
+      const storeId = path.split('/')[1]
+
+      // Verify store ownership
+      const { data: store, error: storeError } = await supabase
+        .from('stores')
+        .select('id, platform_name')
+        .eq('id', storeId)
+        .eq('user_id', user.id)
+        .single()
+
+      if (storeError || !store) {
+        return new Response(
+          JSON.stringify({ error: 'Store not found or access denied' }),
+          { status: 404, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      // Get request body
+      const body = await req.json()
+      const { items, data_type, is_enabled } = body
+
+      if (!items || !Array.isArray(items) || items.length === 0) {
+        return new Response(
+          JSON.stringify({ error: 'items array is required and must not be empty' }),
+          { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+
+      try {
+        const platform = store.platform_name
+        const results = []
+
+        for (const item of items) {
+          const itemDataType = item.data_type || data_type
+          const itemDataId = item.data_id
+          const itemIsEnabled = item.is_enabled !== undefined ? item.is_enabled : is_enabled
+
+          if (!itemDataType || !itemDataId) {
+            continue
+          }
+
+          // Validate data_type
+          if (!['product', 'order', 'customer'].includes(itemDataType)) {
+            continue
+          }
+
+          // Extract metadata from cache tables
+          const tableName = platform === 'woocommerce'
+            ? `woocommerce_${itemDataType}s_cache`
+            : platform === 'shopify'
+            ? `shopify_${itemDataType}s_cache`
+            : `shoprenter_${itemDataType}s_cache`
+
+          const idColumn = platform === 'woocommerce'
+            ? `wc_${itemDataType}_id`
+            : platform === 'shopify'
+            ? `shopify_${itemDataType}_id`
+            : `shoprenter_${itemDataType}_id`
+
+          // Fetch item from cache to extract metadata
+          let metadata = {}
+          const { data: cacheItem } = await supabase
+            .from(tableName)
+            .select('*')
+            .eq('store_id', storeId)
+            .eq(idColumn, itemDataId)
+            .single()
+
+          if (cacheItem) {
+            if (itemDataType === 'product') {
+              if (platform === 'shoprenter') {
+                metadata = {
+                  name: cacheItem.product_data?.name,
+                  sku: cacheItem.product_data?.sku,
+                  price: cacheItem.product_data?.price
+                }
+              } else {
+                metadata = {
+                  name: cacheItem.name,
+                  sku: cacheItem.sku,
+                  price: cacheItem.price
+                }
+              }
+            } else if (itemDataType === 'order') {
+              metadata = {
+                order_number: cacheItem.order_number,
+                customer_name: cacheItem.customer_name,
+                customer_email: cacheItem.customer_email,
+                total: cacheItem.total
+              }
+            } else if (itemDataType === 'customer') {
+              metadata = {
+                email: cacheItem.email,
+                first_name: cacheItem.first_name,
+                last_name: cacheItem.last_name,
+                phone: cacheItem.phone
+              }
+            }
+          }
+
+          // Upsert into store_data_exclusions
+          const { error: upsertError } = await supabase
+            .from('store_data_exclusions')
+            .upsert({
+              store_id: storeId,
+              data_type: itemDataType,
+              data_id: itemDataId,
+              is_enabled: itemIsEnabled,
+              metadata,
+              updated_at: new Date().toISOString()
+            }, {
+              onConflict: 'store_id,data_type,data_id'
+            })
+
+          if (!upsertError) {
+            results.push({
+              data_type: itemDataType,
+              data_id: itemDataId,
+              success: true
+            })
+          } else {
+            results.push({
+              data_type: itemDataType,
+              data_id: itemDataId,
+              success: false,
+              error: upsertError.message
+            })
+          }
+        }
+
+        const successCount = results.filter(r => r.success).length
+        const failCount = results.filter(r => !r.success).length
+
+        return new Response(
+          JSON.stringify({
+            success: true,
+            message: `Batch operation completed: ${successCount} succeeded, ${failCount} failed`,
+            results
+          }),
+          { status: 200, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      } catch (error) {
+        console.error('Error in batch operation:', error)
+        return new Response(
+          JSON.stringify({ error: 'Failed to complete batch operation' }),
+          { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
+        )
+      }
+    }
+
     // GET /api/dashboard/stats - Get dashboard statistics
     if (path === 'dashboard/stats' && req.method === 'GET') {
       // TODO: Implement real dashboard stats calculation