|
|
@@ -44,6 +44,15 @@ import {
|
|
|
} from "../_shared/internal-api-key-auth.ts";
|
|
|
|
|
|
import { getAdapters } from "../_shared/platform-adapters.ts";
|
|
|
+import {
|
|
|
+ createSuccessResponse,
|
|
|
+ createListResponse,
|
|
|
+ createErrorResponse,
|
|
|
+ toHttpResponse,
|
|
|
+ calculateHasMore,
|
|
|
+ generateRequestId,
|
|
|
+ type UnifiedApiResponse,
|
|
|
+} from "../_shared/unified-response.ts";
|
|
|
|
|
|
// Import platform-specific clients
|
|
|
import * as ShopifyClient from "../_shared/shopify-client.ts";
|
|
|
@@ -52,21 +61,6 @@ 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
|
|
|
@@ -137,10 +131,7 @@ async function handler(req: Request): Promise<Response> {
|
|
|
|
|
|
// Only allow GET requests
|
|
|
if (req.method !== "GET") {
|
|
|
- return new Response(
|
|
|
- JSON.stringify({ error: "Method not allowed" }),
|
|
|
- { status: 405, headers: { "Content-Type": "application/json" } }
|
|
|
- );
|
|
|
+ return createErrorResponse("METHOD_NOT_ALLOWED", "Method not allowed", undefined, 405);
|
|
|
}
|
|
|
|
|
|
// Authenticate request
|
|
|
@@ -157,10 +148,7 @@ async function handler(req: Request): Promise<Response> {
|
|
|
|
|
|
// 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" } }
|
|
|
- );
|
|
|
+ return createErrorResponse("INVALID_ENDPOINT", "Invalid endpoint", undefined, 404);
|
|
|
}
|
|
|
|
|
|
const resource = pathParts[1]; // 'stores', 'customers', 'orders', or 'products'
|
|
|
@@ -180,33 +168,30 @@ async function handler(req: Request): Promise<Response> {
|
|
|
|
|
|
// 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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "MISSING_STORE_ID",
|
|
|
+ "Missing required parameter: store_id",
|
|
|
+ undefined,
|
|
|
+ 400
|
|
|
);
|
|
|
}
|
|
|
|
|
|
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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "INVALID_RESOURCE",
|
|
|
+ "Invalid resource. Must be stores, customers, orders, or products",
|
|
|
+ undefined,
|
|
|
+ 400
|
|
|
);
|
|
|
}
|
|
|
|
|
|
// 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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "INVALID_PAGINATION",
|
|
|
+ "Invalid pagination parameters. Page must be >= 1, limit must be 1-100",
|
|
|
+ undefined,
|
|
|
+ 400
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -225,12 +210,11 @@ async function handler(req: Request): Promise<Response> {
|
|
|
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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "STORE_NOT_FOUND",
|
|
|
+ "Store not found or access denied",
|
|
|
+ undefined,
|
|
|
+ 404
|
|
|
);
|
|
|
}
|
|
|
|
|
|
@@ -240,19 +224,17 @@ async function handler(req: Request): Promise<Response> {
|
|
|
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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "ACCESS_DENIED",
|
|
|
+ `Access to ${resource} is not enabled for this store`,
|
|
|
+ { hint: "Enable access in store settings" },
|
|
|
+ 403
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// Fetch data based on resource and platform
|
|
|
- let responseData: ApiResponse;
|
|
|
+ let responseData: UnifiedApiResponse<any>;
|
|
|
|
|
|
try {
|
|
|
switch (store.platform_name.toLowerCase()) {
|
|
|
@@ -263,7 +245,8 @@ async function handler(req: Request): Promise<Response> {
|
|
|
resourceId,
|
|
|
page,
|
|
|
limit,
|
|
|
- status
|
|
|
+ status,
|
|
|
+ authContext
|
|
|
);
|
|
|
break;
|
|
|
case "woocommerce":
|
|
|
@@ -273,7 +256,8 @@ async function handler(req: Request): Promise<Response> {
|
|
|
resourceId,
|
|
|
page,
|
|
|
limit,
|
|
|
- status
|
|
|
+ status,
|
|
|
+ authContext
|
|
|
);
|
|
|
break;
|
|
|
case "shoprenter":
|
|
|
@@ -283,35 +267,31 @@ async function handler(req: Request): Promise<Response> {
|
|
|
resourceId,
|
|
|
page,
|
|
|
limit,
|
|
|
- status
|
|
|
+ status,
|
|
|
+ authContext
|
|
|
);
|
|
|
break;
|
|
|
default:
|
|
|
- return new Response(
|
|
|
- JSON.stringify({
|
|
|
- error: `Unsupported platform: ${store.platform_name}`,
|
|
|
- code: "UNSUPPORTED_PLATFORM",
|
|
|
- }),
|
|
|
- { status: 400, headers: { "Content-Type": "application/json" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "UNSUPPORTED_PLATFORM",
|
|
|
+ `Unsupported platform: ${store.platform_name}`,
|
|
|
+ undefined,
|
|
|
+ 400
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- const response = new Response(JSON.stringify(responseData), {
|
|
|
- status: 200,
|
|
|
- headers: { "Content-Type": "application/json" },
|
|
|
- });
|
|
|
+ const response = toHttpResponse(responseData, 200);
|
|
|
|
|
|
// 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" } }
|
|
|
+ return createErrorResponse(
|
|
|
+ "FETCH_ERROR",
|
|
|
+ error instanceof Error ? error.message : "Failed to fetch data",
|
|
|
+ error instanceof Error ? { stack: error.stack } : undefined,
|
|
|
+ 500
|
|
|
);
|
|
|
}
|
|
|
}
|
|
|
@@ -336,25 +316,31 @@ async function handleListStores(
|
|
|
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 createErrorResponse(
|
|
|
+ "FETCH_ERROR",
|
|
|
+ "Failed to fetch stores",
|
|
|
+ error,
|
|
|
+ 500
|
|
|
);
|
|
|
}
|
|
|
|
|
|
- return new Response(
|
|
|
- JSON.stringify({
|
|
|
- success: true,
|
|
|
- data: stores,
|
|
|
- count: stores.length,
|
|
|
+ const response = createListResponse(
|
|
|
+ stores,
|
|
|
+ {
|
|
|
+ resource_type: "stores",
|
|
|
auth_type: authContext.type,
|
|
|
fetched_at: new Date().toISOString(),
|
|
|
- }),
|
|
|
- { status: 200, headers: { "Content-Type": "application/json" } }
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ },
|
|
|
+ {
|
|
|
+ page: 1,
|
|
|
+ limit: stores.length,
|
|
|
+ total: stores.length,
|
|
|
+ has_more: false,
|
|
|
+ }
|
|
|
);
|
|
|
+
|
|
|
+ return toHttpResponse(response, 200);
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
@@ -377,8 +363,9 @@ async function fetchShopifyData(
|
|
|
resourceId: string | undefined,
|
|
|
page: number,
|
|
|
limit: number,
|
|
|
- status: string | null
|
|
|
-): Promise<ApiResponse> {
|
|
|
+ status: string | null,
|
|
|
+ authContext: AuthContext
|
|
|
+): Promise<UnifiedApiResponse<any>> {
|
|
|
const adapters = getAdapters("shopify");
|
|
|
|
|
|
if (resourceId) {
|
|
|
@@ -406,12 +393,14 @@ async function fetchShopifyData(
|
|
|
data = adapters.product(response.product);
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- success: true,
|
|
|
+ return createSuccessResponse(data, {
|
|
|
platform: "shopify",
|
|
|
- data,
|
|
|
+ store_id: store.id,
|
|
|
+ resource_type: resource as any,
|
|
|
+ auth_type: authContext.type,
|
|
|
fetched_at: new Date().toISOString(),
|
|
|
- };
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ });
|
|
|
} else {
|
|
|
// Fetch list
|
|
|
const sinceId = page > 1 ? undefined : undefined; // Shopify uses since_id for pagination
|
|
|
@@ -431,17 +420,22 @@ async function fetchShopifyData(
|
|
|
data = rawData.map((item: any) => adapters.product(item));
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- platform: "shopify",
|
|
|
+ return createListResponse(
|
|
|
data,
|
|
|
- pagination: {
|
|
|
+ {
|
|
|
+ platform: "shopify",
|
|
|
+ store_id: store.id,
|
|
|
+ resource_type: resource as any,
|
|
|
+ auth_type: authContext.type,
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ },
|
|
|
+ {
|
|
|
page,
|
|
|
limit,
|
|
|
- has_more: (rawData?.length || 0) === limit,
|
|
|
- },
|
|
|
- fetched_at: new Date().toISOString(),
|
|
|
- };
|
|
|
+ has_more: calculateHasMore(rawData?.length || 0, limit),
|
|
|
+ }
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -454,8 +448,9 @@ async function fetchWooCommerceData(
|
|
|
resourceId: string | undefined,
|
|
|
page: number,
|
|
|
limit: number,
|
|
|
- status: string | null
|
|
|
-): Promise<ApiResponse> {
|
|
|
+ status: string | null,
|
|
|
+ authContext: AuthContext
|
|
|
+): Promise<UnifiedApiResponse<any>> {
|
|
|
const adapters = getAdapters("woocommerce");
|
|
|
|
|
|
if (resourceId) {
|
|
|
@@ -474,12 +469,14 @@ async function fetchWooCommerceData(
|
|
|
data = adapters.product(rawData);
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- success: true,
|
|
|
+ return createSuccessResponse(data, {
|
|
|
platform: "woocommerce",
|
|
|
- data,
|
|
|
+ store_id: store.id,
|
|
|
+ resource_type: resource as any,
|
|
|
+ auth_type: authContext.type,
|
|
|
fetched_at: new Date().toISOString(),
|
|
|
- };
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ });
|
|
|
} else {
|
|
|
// Fetch list
|
|
|
let data;
|
|
|
@@ -501,17 +498,22 @@ async function fetchWooCommerceData(
|
|
|
data = rawData.map((item: any) => adapters.product(item));
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- platform: "woocommerce",
|
|
|
+ return createListResponse(
|
|
|
data,
|
|
|
- pagination: {
|
|
|
+ {
|
|
|
+ platform: "woocommerce",
|
|
|
+ store_id: store.id,
|
|
|
+ resource_type: resource as any,
|
|
|
+ auth_type: authContext.type,
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ },
|
|
|
+ {
|
|
|
page,
|
|
|
limit,
|
|
|
- has_more: (rawData?.length || 0) === limit,
|
|
|
- },
|
|
|
- fetched_at: new Date().toISOString(),
|
|
|
- };
|
|
|
+ has_more: calculateHasMore(rawData?.length || 0, limit),
|
|
|
+ }
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -524,8 +526,9 @@ async function fetchShopRenterData(
|
|
|
resourceId: string | undefined,
|
|
|
page: number,
|
|
|
limit: number,
|
|
|
- status: string | null
|
|
|
-): Promise<ApiResponse> {
|
|
|
+ status: string | null,
|
|
|
+ authContext: AuthContext
|
|
|
+): Promise<UnifiedApiResponse<any>> {
|
|
|
const adapters = getAdapters("shoprenter");
|
|
|
|
|
|
if (resourceId) {
|
|
|
@@ -551,17 +554,22 @@ async function fetchShopRenterData(
|
|
|
data = rawData.map((item: any) => adapters.product(item));
|
|
|
}
|
|
|
|
|
|
- return {
|
|
|
- success: true,
|
|
|
- platform: "shoprenter",
|
|
|
+ return createListResponse(
|
|
|
data,
|
|
|
- pagination: {
|
|
|
+ {
|
|
|
+ platform: "shoprenter",
|
|
|
+ store_id: store.id,
|
|
|
+ resource_type: resource as any,
|
|
|
+ auth_type: authContext.type,
|
|
|
+ fetched_at: new Date().toISOString(),
|
|
|
+ request_id: generateRequestId(),
|
|
|
+ },
|
|
|
+ {
|
|
|
page,
|
|
|
limit,
|
|
|
- has_more: (rawData?.length || 0) === limit,
|
|
|
- },
|
|
|
- fetched_at: new Date().toISOString(),
|
|
|
- };
|
|
|
+ has_more: calculateHasMore(rawData?.length || 0, limit),
|
|
|
+ }
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|