|
@@ -0,0 +1,569 @@
|
|
|
|
|
+/**
|
|
|
|
|
+ * Shop Data API - Unified REST API for E-commerce Data Access (Internal + User API Keys)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Provides real-time access to customer, order, and product data from ALL connected
|
|
|
|
|
+ * webshops using internal (non-user-based) API keys OR user-based API keys.
|
|
|
|
|
+ *
|
|
|
|
|
+ * Internal API Keys: System-level access to all webshops (not tied to specific user)
|
|
|
|
|
+ * User API Keys: User-specific access (existing functionality)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Related: Issue #50 - Implement non user based API keys
|
|
|
|
|
+ *
|
|
|
|
|
+ * Authentication: API Key (Bearer token)
|
|
|
|
|
+ * - int_shopcall_xxx (internal keys - access all shops)
|
|
|
|
|
+ * - api_shopcall_xxx (user keys - access only user's shops)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Rate Limit: 200 requests/minute (internal), 100 requests/minute (user)
|
|
|
|
|
+ *
|
|
|
|
|
+ * Endpoints:
|
|
|
|
|
+ * - GET /shop-data-api/stores - List all stores (internal only) or user's stores
|
|
|
|
|
+ * - GET /shop-data-api/customers?store_id={uuid}&page=1&limit=25
|
|
|
|
|
+ * - GET /shop-data-api/customers/{id}?store_id={uuid}
|
|
|
|
|
+ * - GET /shop-data-api/orders?store_id={uuid}&status=completed&page=1
|
|
|
|
|
+ * - GET /shop-data-api/orders/{id}?store_id={uuid}
|
|
|
|
|
+ * - GET /shop-data-api/products?store_id={uuid}&page=1
|
|
|
|
|
+ * - GET /shop-data-api/products/{id}?store_id={uuid}
|
|
|
|
|
+ */
|
|
|
|
|
+
|
|
|
|
|
+import { createClient } from "https://esm.sh/@supabase/supabase-js@2.39.7";
|
|
|
|
|
+import { wrapHandler } from "../_shared/error-handler.ts";
|
|
|
|
|
+
|
|
|
|
|
+// Import authentication for both API key types
|
|
|
|
|
+import {
|
|
|
|
|
+ requireApiKey,
|
|
|
|
|
+ createApiKeyErrorResponse,
|
|
|
|
|
+ addRateLimitHeaders,
|
|
|
|
|
+ extractApiKey,
|
|
|
|
|
+} from "../_shared/api-key-auth.ts";
|
|
|
|
|
+
|
|
|
|
|
+import {
|
|
|
|
|
+ requireInternalApiKey,
|
|
|
|
|
+ createInternalApiKeyErrorResponse,
|
|
|
|
|
+ addInternalRateLimitHeaders,
|
|
|
|
|
+ extractInternalApiKey,
|
|
|
|
|
+} from "../_shared/internal-api-key-auth.ts";
|
|
|
|
|
+
|
|
|
|
|
+import { getAdapters } from "../_shared/platform-adapters.ts";
|
|
|
|
|
+
|
|
|
|
|
+// Import platform-specific clients
|
|
|
|
|
+import * as ShopifyClient from "../_shared/shopify-client.ts";
|
|
|
|
|
+import * as WooCommerceClient from "../_shared/woocommerce-client.ts";
|
|
|
|
|
+import * as ShopRenterClient from "../_shared/shoprenter-client.ts";
|
|
|
|
|
+
|
|
|
|
|
+const FUNCTION_NAME = "shop-data-api";
|
|
|
|
|
+
|
|
|
|
|
+interface PaginationMeta {
|
|
|
|
|
+ page: number;
|
|
|
|
|
+ limit: number;
|
|
|
|
|
+ total?: number;
|
|
|
|
|
+ has_more?: boolean;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ApiResponse {
|
|
|
|
|
+ success: boolean;
|
|
|
|
|
+ platform?: string;
|
|
|
|
|
+ data: unknown;
|
|
|
|
|
+ pagination?: PaginationMeta;
|
|
|
|
|
+ fetched_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface AuthContext {
|
|
|
|
|
+ type: "user" | "internal";
|
|
|
|
|
+ userId?: string; // Only for user API keys
|
|
|
|
|
+ apiKey: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Authenticate request with either user or internal API key
|
|
|
|
|
+ */
|
|
|
|
|
+async function authenticateRequest(
|
|
|
|
|
+ req: Request,
|
|
|
|
|
+ supabaseAdmin: any
|
|
|
|
|
+): Promise<{ context: AuthContext | null; errorResponse: Response | null }> {
|
|
|
|
|
+ // Try internal API key first (int_shopcall_xxx)
|
|
|
|
|
+ const internalKey = extractInternalApiKey(req);
|
|
|
|
|
+ if (internalKey) {
|
|
|
|
|
+ const result = await requireInternalApiKey(req, supabaseAdmin);
|
|
|
|
|
+ if (!result.valid) {
|
|
|
|
|
+ return { context: null, errorResponse: createInternalApiKeyErrorResponse(result) };
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ context: { type: "internal", apiKey: internalKey },
|
|
|
|
|
+ errorResponse: null,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Try user API key (api_shopcall_xxx)
|
|
|
|
|
+ const userKey = extractApiKey(req);
|
|
|
|
|
+ if (userKey) {
|
|
|
|
|
+ const result = await requireApiKey(req, supabaseAdmin, "webshop_data");
|
|
|
|
|
+ if (!result.valid) {
|
|
|
|
|
+ return { context: null, errorResponse: createApiKeyErrorResponse(result) };
|
|
|
|
|
+ }
|
|
|
|
|
+ return {
|
|
|
|
|
+ context: { type: "user", userId: result.userId!, apiKey: userKey },
|
|
|
|
|
+ errorResponse: null,
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // No valid API key found
|
|
|
|
|
+ return {
|
|
|
|
|
+ context: null,
|
|
|
|
|
+ errorResponse: new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Missing API key. Provide either int_shopcall_xxx or api_shopcall_xxx",
|
|
|
|
|
+ code: "MISSING_API_KEY",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 401, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ ),
|
|
|
|
|
+ };
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Main handler function
|
|
|
|
|
+ */
|
|
|
|
|
+async function handler(req: Request): Promise<Response> {
|
|
|
|
|
+ const supabaseUrl = Deno.env.get("SUPABASE_URL");
|
|
|
|
|
+ const supabaseServiceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY");
|
|
|
|
|
+
|
|
|
|
|
+ if (!supabaseUrl || !supabaseServiceKey) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: "Server configuration error" }),
|
|
|
|
|
+ { status: 500, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const supabaseAdmin = createClient(supabaseUrl, supabaseServiceKey);
|
|
|
|
|
+
|
|
|
|
|
+ // Only allow GET requests
|
|
|
|
|
+ if (req.method !== "GET") {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: "Method not allowed" }),
|
|
|
|
|
+ { status: 405, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Authenticate request
|
|
|
|
|
+ const { context, errorResponse } = await authenticateRequest(req, supabaseAdmin);
|
|
|
|
|
+ if (errorResponse) {
|
|
|
|
|
+ return errorResponse;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const authContext = context!;
|
|
|
|
|
+
|
|
|
|
|
+ // Parse URL and query parameters
|
|
|
|
|
+ const url = new URL(req.url);
|
|
|
|
|
+ const pathParts = url.pathname.split("/").filter((p) => p);
|
|
|
|
|
+
|
|
|
|
|
+ // Expected path: /shop-data-api/{resource}/{id?}
|
|
|
|
|
+ if (pathParts[0] !== "shop-data-api") {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({ error: "Invalid endpoint" }),
|
|
|
|
|
+ { status: 404, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const resource = pathParts[1]; // 'stores', 'customers', 'orders', or 'products'
|
|
|
|
|
+ const resourceId = pathParts[2]; // Optional ID
|
|
|
|
|
+
|
|
|
|
|
+ // Handle special endpoint: list stores
|
|
|
|
|
+ if (resource === "stores") {
|
|
|
|
|
+ const response = await handleListStores(supabaseAdmin, authContext);
|
|
|
|
|
+ return addRateLimitHeadersToResponse(response, authContext);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Get query parameters
|
|
|
|
|
+ const storeId = url.searchParams.get("store_id");
|
|
|
|
|
+ const page = parseInt(url.searchParams.get("page") || "1");
|
|
|
|
|
+ const limit = parseInt(url.searchParams.get("limit") || "25");
|
|
|
|
|
+ const status = url.searchParams.get("status"); // For orders
|
|
|
|
|
+
|
|
|
|
|
+ // Validate required parameters
|
|
|
|
|
+ if (!storeId) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Missing required parameter: store_id",
|
|
|
|
|
+ code: "MISSING_STORE_ID",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 400, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!["customers", "orders", "products"].includes(resource)) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Invalid resource. Must be stores, customers, orders, or products",
|
|
|
|
|
+ code: "INVALID_RESOURCE",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 400, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate pagination parameters
|
|
|
|
|
+ if (page < 1 || limit < 1 || limit > 100) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Invalid pagination parameters. Page must be >= 1, limit must be 1-100",
|
|
|
|
|
+ code: "INVALID_PAGINATION",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 400, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch store and verify access
|
|
|
|
|
+ const storeQuery = supabaseAdmin
|
|
|
|
|
+ .from("stores")
|
|
|
|
|
+ .select("*")
|
|
|
|
|
+ .eq("id", storeId)
|
|
|
|
|
+ .eq("is_active", true);
|
|
|
|
|
+
|
|
|
|
|
+ // For user API keys, restrict to user's stores
|
|
|
|
|
+ if (authContext.type === "user") {
|
|
|
|
|
+ storeQuery.eq("user_id", authContext.userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { data: store, error: storeError } = await storeQuery.single();
|
|
|
|
|
+
|
|
|
|
|
+ if (storeError || !store) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Store not found or access denied",
|
|
|
|
|
+ code: "STORE_NOT_FOUND",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 404, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check data access permissions (only for user keys, internal keys have full access)
|
|
|
|
|
+ if (authContext.type === "user") {
|
|
|
|
|
+ const permissions = store.data_access_permissions || {};
|
|
|
|
|
+ const dataType = resource.slice(0, -1); // Remove 's' (customers -> customer)
|
|
|
|
|
+
|
|
|
|
|
+ if (!permissions[`allow_${dataType}_access`]) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: `Access to ${resource} is not enabled for this store`,
|
|
|
|
|
+ code: "ACCESS_DENIED",
|
|
|
|
|
+ hint: "Enable access in store settings",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 403, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch data based on resource and platform
|
|
|
|
|
+ let responseData: ApiResponse;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ switch (store.platform_name.toLowerCase()) {
|
|
|
|
|
+ case "shopify":
|
|
|
|
|
+ responseData = await fetchShopifyData(
|
|
|
|
|
+ store,
|
|
|
|
|
+ resource,
|
|
|
|
|
+ resourceId,
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ status
|
|
|
|
|
+ );
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "woocommerce":
|
|
|
|
|
+ responseData = await fetchWooCommerceData(
|
|
|
|
|
+ store,
|
|
|
|
|
+ resource,
|
|
|
|
|
+ resourceId,
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ status
|
|
|
|
|
+ );
|
|
|
|
|
+ break;
|
|
|
|
|
+ case "shoprenter":
|
|
|
|
|
+ responseData = await fetchShopRenterData(
|
|
|
|
|
+ store,
|
|
|
|
|
+ resource,
|
|
|
|
|
+ resourceId,
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ status
|
|
|
|
|
+ );
|
|
|
|
|
+ break;
|
|
|
|
|
+ default:
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: `Unsupported platform: ${store.platform_name}`,
|
|
|
|
|
+ code: "UNSUPPORTED_PLATFORM",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 400, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = new Response(JSON.stringify(responseData), {
|
|
|
|
|
+ status: 200,
|
|
|
|
|
+ headers: { "Content-Type": "application/json" },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Add rate limit headers
|
|
|
|
|
+ return addRateLimitHeadersToResponse(response, authContext);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error(`Error fetching ${resource}:`, error);
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: error instanceof Error ? error.message : "Failed to fetch data",
|
|
|
|
|
+ code: "FETCH_ERROR",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 500, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Handle listing stores
|
|
|
|
|
+ */
|
|
|
|
|
+async function handleListStores(
|
|
|
|
|
+ supabaseAdmin: any,
|
|
|
|
|
+ authContext: AuthContext
|
|
|
|
|
+): Promise<Response> {
|
|
|
|
|
+ const query = supabaseAdmin
|
|
|
|
|
+ .from("stores")
|
|
|
|
|
+ .select("id, store_name, store_url, platform_name, is_active, connected_at, sync_status, user_id")
|
|
|
|
|
+ .eq("is_active", true);
|
|
|
|
|
+
|
|
|
|
|
+ // For user API keys, only show user's stores
|
|
|
|
|
+ if (authContext.type === "user") {
|
|
|
|
|
+ query.eq("user_id", authContext.userId);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const { data: stores, error } = await query;
|
|
|
|
|
+
|
|
|
|
|
+ if (error) {
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ error: "Failed to fetch stores",
|
|
|
|
|
+ code: "FETCH_ERROR",
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 500, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return new Response(
|
|
|
|
|
+ JSON.stringify({
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ data: stores,
|
|
|
|
|
+ count: stores.length,
|
|
|
|
|
+ auth_type: authContext.type,
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ }),
|
|
|
|
|
+ { status: 200, headers: { "Content-Type": "application/json" } }
|
|
|
|
|
+ );
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Add appropriate rate limit headers based on auth context
|
|
|
|
|
+ */
|
|
|
|
|
+function addRateLimitHeadersToResponse(response: Response, authContext: AuthContext): Response {
|
|
|
|
|
+ if (authContext.type === "internal") {
|
|
|
|
|
+ return addInternalRateLimitHeaders(response, authContext.apiKey);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return addRateLimitHeaders(response, authContext.apiKey);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Fetch data from Shopify
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchShopifyData(
|
|
|
|
|
+ store: any,
|
|
|
|
|
+ resource: string,
|
|
|
|
|
+ resourceId: string | undefined,
|
|
|
|
|
+ page: number,
|
|
|
|
|
+ limit: number,
|
|
|
|
|
+ status: string | null
|
|
|
|
|
+): Promise<ApiResponse> {
|
|
|
|
|
+ const adapters = getAdapters("shopify");
|
|
|
|
|
+
|
|
|
|
|
+ if (resourceId) {
|
|
|
|
|
+ // Fetch single item
|
|
|
|
|
+ const itemId = parseInt(resourceId);
|
|
|
|
|
+ let data;
|
|
|
|
|
+
|
|
|
|
|
+ if (resource === "customers") {
|
|
|
|
|
+ const response = await ShopifyClient.shopifyApiRequest(
|
|
|
|
|
+ store.id,
|
|
|
|
|
+ `/customers/${itemId}.json`
|
|
|
|
|
+ );
|
|
|
|
|
+ data = adapters.customer(response.customer);
|
|
|
|
|
+ } else if (resource === "orders") {
|
|
|
|
|
+ const response = await ShopifyClient.shopifyApiRequest(
|
|
|
|
|
+ store.id,
|
|
|
|
|
+ `/orders/${itemId}.json`
|
|
|
|
|
+ );
|
|
|
|
|
+ data = adapters.order(response.order);
|
|
|
|
|
+ } else if (resource === "products") {
|
|
|
|
|
+ const response = await ShopifyClient.shopifyApiRequest(
|
|
|
|
|
+ store.id,
|
|
|
|
|
+ `/products/${itemId}.json`
|
|
|
|
|
+ );
|
|
|
|
|
+ data = adapters.product(response.product);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ platform: "shopify",
|
|
|
|
|
+ data,
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fetch list
|
|
|
|
|
+ const sinceId = page > 1 ? undefined : undefined; // Shopify uses since_id for pagination
|
|
|
|
|
+ let data;
|
|
|
|
|
+ let rawData;
|
|
|
|
|
+
|
|
|
|
|
+ if (resource === "customers") {
|
|
|
|
|
+ rawData = await ShopifyClient.fetchCustomers(store.id, limit, sinceId);
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.customer(item));
|
|
|
|
|
+ } else if (resource === "orders") {
|
|
|
|
|
+ let endpoint = `/orders.json?limit=${limit}&status=${status || "any"}`;
|
|
|
|
|
+ const response = await ShopifyClient.shopifyApiRequest(store.id, endpoint);
|
|
|
|
|
+ rawData = response.orders || [];
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.order(item));
|
|
|
|
|
+ } else if (resource === "products") {
|
|
|
|
|
+ rawData = await ShopifyClient.fetchProducts(store.id, limit, sinceId);
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.product(item));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ platform: "shopify",
|
|
|
|
|
+ data,
|
|
|
|
|
+ pagination: {
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ has_more: (rawData?.length || 0) === limit,
|
|
|
|
|
+ },
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Fetch data from WooCommerce
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchWooCommerceData(
|
|
|
|
|
+ store: any,
|
|
|
|
|
+ resource: string,
|
|
|
|
|
+ resourceId: string | undefined,
|
|
|
|
|
+ page: number,
|
|
|
|
|
+ limit: number,
|
|
|
|
|
+ status: string | null
|
|
|
|
|
+): Promise<ApiResponse> {
|
|
|
|
|
+ const adapters = getAdapters("woocommerce");
|
|
|
|
|
+
|
|
|
|
|
+ if (resourceId) {
|
|
|
|
|
+ // Fetch single item
|
|
|
|
|
+ const itemId = parseInt(resourceId);
|
|
|
|
|
+ let data;
|
|
|
|
|
+
|
|
|
|
|
+ if (resource === "customers") {
|
|
|
|
|
+ const rawData = await WooCommerceClient.fetchCustomer(store.id, itemId);
|
|
|
|
|
+ data = adapters.customer(rawData);
|
|
|
|
|
+ } else if (resource === "orders") {
|
|
|
|
|
+ const rawData = await WooCommerceClient.fetchOrder(store.id, itemId);
|
|
|
|
|
+ data = adapters.order(rawData);
|
|
|
|
|
+ } else if (resource === "products") {
|
|
|
|
|
+ const rawData = await WooCommerceClient.fetchProduct(store.id, itemId);
|
|
|
|
|
+ data = adapters.product(rawData);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ platform: "woocommerce",
|
|
|
|
|
+ data,
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fetch list
|
|
|
|
|
+ let data;
|
|
|
|
|
+ let rawData;
|
|
|
|
|
+
|
|
|
|
|
+ if (resource === "customers") {
|
|
|
|
|
+ rawData = await WooCommerceClient.fetchCustomers(store.id, page, limit);
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.customer(item));
|
|
|
|
|
+ } else if (resource === "orders") {
|
|
|
|
|
+ rawData = await WooCommerceClient.fetchOrders(
|
|
|
|
|
+ store.id,
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ status || undefined
|
|
|
|
|
+ );
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.order(item));
|
|
|
|
|
+ } else if (resource === "products") {
|
|
|
|
|
+ rawData = await WooCommerceClient.fetchProducts(store.id, page, limit);
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.product(item));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ platform: "woocommerce",
|
|
|
|
|
+ data,
|
|
|
|
|
+ pagination: {
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ has_more: (rawData?.length || 0) === limit,
|
|
|
|
|
+ },
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * Fetch data from ShopRenter
|
|
|
|
|
+ */
|
|
|
|
|
+async function fetchShopRenterData(
|
|
|
|
|
+ store: any,
|
|
|
|
|
+ resource: string,
|
|
|
|
|
+ resourceId: string | undefined,
|
|
|
|
|
+ page: number,
|
|
|
|
|
+ limit: number,
|
|
|
|
|
+ status: string | null
|
|
|
|
|
+): Promise<ApiResponse> {
|
|
|
|
|
+ const adapters = getAdapters("shoprenter");
|
|
|
|
|
+
|
|
|
|
|
+ if (resourceId) {
|
|
|
|
|
+ // Fetch single item - ShopRenter client doesn't have individual fetch methods
|
|
|
|
|
+ // So we'll fetch the list and filter (or implement individual fetches)
|
|
|
|
|
+ throw new Error("Single item fetch not yet implemented for ShopRenter");
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Fetch list
|
|
|
|
|
+ let data;
|
|
|
|
|
+ let rawData;
|
|
|
|
|
+
|
|
|
|
|
+ if (resource === "customers") {
|
|
|
|
|
+ const response = await ShopRenterClient.fetchCustomers(store.id, page, limit);
|
|
|
|
|
+ rawData = response.items || [];
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.customer(item));
|
|
|
|
|
+ } else if (resource === "orders") {
|
|
|
|
|
+ const response = await ShopRenterClient.fetchOrders(store.id, page, limit);
|
|
|
|
|
+ rawData = response.items || [];
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.order(item));
|
|
|
|
|
+ } else if (resource === "products") {
|
|
|
|
|
+ const response = await ShopRenterClient.fetchProducts(store.id, page, limit);
|
|
|
|
|
+ rawData = response.items || [];
|
|
|
|
|
+ data = rawData.map((item: any) => adapters.product(item));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return {
|
|
|
|
|
+ success: true,
|
|
|
|
|
+ platform: "shoprenter",
|
|
|
|
|
+ data,
|
|
|
|
|
+ pagination: {
|
|
|
|
|
+ page,
|
|
|
|
|
+ limit,
|
|
|
|
|
+ has_more: (rawData?.length || 0) === limit,
|
|
|
|
|
+ },
|
|
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+// Wrap handler with error handling
|
|
|
|
|
+Deno.serve(wrapHandler(FUNCTION_NAME, handler));
|