UNPKG

google-retail-api-client

Version:
224 lines (223 loc) 9.02 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.RetailClient = void 0; const google_auth_library_1 = require("google-auth-library"); const errors_1 = require("../utils/errors"); class RetailClient { constructor(config) { this.baseUrl = "https://retail.googleapis.com/v2alpha"; this.validateConfig(config); this.projectId = config.credentials.project_id; this.location = config.location || "global"; this.catalogId = config.catalogId || "default_catalog"; this.timeout = config.timeout || 10000; // Initialize GoogleAuth with service account credentials this.auth = new google_auth_library_1.GoogleAuth({ credentials: config.credentials, scopes: ['https://www.googleapis.com/auth/cloud-platform'], projectId: this.projectId, }); } validateConfig(config) { if (!config.credentials) { throw new errors_1.RetailApiError("Service account credentials are required", 400, { field: "credentials", message: "Missing required configuration" }); } const requiredFields = [ 'type', 'project_id', 'private_key', 'client_email' ]; for (const field of requiredFields) { if (!config.credentials[field]) { throw new errors_1.RetailApiError(`Missing required credential field: ${field}`, 400, { field, message: "Missing required credential field" }); } } if (config.credentials.type !== 'service_account') { throw new errors_1.RetailApiError("Invalid credentials type. Must be 'service_account'", 400, { field: "type", message: "Invalid credential type" }); } } getPlacementPath(placementId) { return `projects/${this.projectId}/locations/${this.location}/catalogs/${this.catalogId}/servingConfigs/${placementId}`; } get searchServicePath() { return `${this.baseUrl}/${this.getPlacementPath("default_search")}:search`; } async getAuthHeaders() { try { const client = await this.auth.getClient(); const token = await client.getAccessToken(); if (!token.token) { throw new errors_1.RetailApiError('No valid authentication token found', 401, { action: "Please run: gcloud auth login --update-adc" }); } return { Authorization: `Bearer ${token.token}`, "Content-Type": "application/json", }; } catch (error) { throw new errors_1.RetailApiError('Authentication failed', 401, { originalError: error, action: "Please ensure you have run: gcloud auth login --update-adc" }); } } async fetchWithTimeout(url, options) { var _a; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.timeout); try { const response = await fetch(url, { ...options, signal: controller.signal }); if (!response.ok) { const errorData = await response.json().catch(() => ({})); throw new errors_1.RetailApiError(((_a = errorData.error) === null || _a === void 0 ? void 0 : _a.message) || response.statusText, response.status, { response: errorData, request: { url, method: options.method } }); } return response; } finally { clearTimeout(timeoutId); } } async makeRequest(url, method, body) { const headers = await this.getAuthHeaders(); try { const response = await this.fetchWithTimeout(url, { method, headers: { ...headers, 'Content-Type': 'application/json', }, body: body ? JSON.stringify(body) : undefined }); const data = await response.json(); return data; } catch (error) { if (error instanceof errors_1.RetailApiError) { throw error; } if (error instanceof Error && error.name === 'AbortError') { throw new errors_1.RetailApiError('Request timeout', 408, { originalError: error }); } throw new errors_1.RetailApiError(error instanceof Error ? error.message : 'Unknown error', 500, { originalError: error }); } } /** * Search for products in the retail catalog */ async search({ query = "", filter = "", pageSize = 10, offset = 0, orderBy = "", visitorId = '', }) { const searchRequest = { query, filter, pageSize, offset, orderBy, dynamicFacetSpec: { mode: "ENABLED", }, useExpensiveRerankDeadline: true, availabilityOption: { enablePrimarySearch: true, enableSecondarySearch: false, enableAvailabilityCache: false, }, conversationalSearchSpec: { conversationPlanRequested: true, }, searchMode: "SEARCH_MODE_UNSPECIFIED", visitorId, }; const data = await this.makeRequest(this.searchServicePath, 'POST', searchRequest); return { results: (data.results || []).map(result => { var _a; return ({ ...result.product, id: result.id, metadata: { ranking_score: (_a = result.metadata) === null || _a === void 0 ? void 0 : _a.ranking_score } }); }), totalSize: data.totalSize || 0, facets: data.facets || [], nextPageToken: data.nextPageToken, }; } /** * Get product predictions/recommendations */ async getPredictions({ productId, visitorId = '', pageSize = 12, filter = "", placementId = "default_recommendation", eventType = "home-page-view" }) { var _a; const predictionRequest = { userEvent: { eventType, visitorId, productDetails: productId ? [{ product: { id: productId } }] : undefined, }, pageSize, filter, params: { returnProduct: false, returnScore: true, filterSyntaxV2: true, }, }; const predictionUrl = `${this.baseUrl}/${this.getPlacementPath(placementId)}:predict`; const predictionData = await this.makeRequest(predictionUrl, 'POST', predictionRequest); // Extract SKUs from prediction results const skus = ((_a = predictionData.results) === null || _a === void 0 ? void 0 : _a.map(result => result.id)) || []; // If we have SKUs, fetch their details using search if (skus.length > 0) { const searchResponse = await this.search({ filter: `productId: ANY("${skus.join('","')}")`, pageSize: skus.length }); return this.transformPredictionResponse({ results: searchResponse.results.map(result => { var _a; return ({ id: result.id, metadata: result, ranking_score: ((_a = result.metadata) === null || _a === void 0 ? void 0 : _a.ranking_score) || 0 }); }), recommendationToken: predictionData.recommendationToken, outOfStockProducts: predictionData.outOfStockProducts || [], missingProducts: predictionData.missingProducts || [] }); } return this.transformPredictionResponse({ results: [], recommendationToken: predictionData.recommendationToken, outOfStockProducts: predictionData.outOfStockProducts || [], missingProducts: predictionData.missingProducts || [] }); } transformPredictionResponse(response) { return { results: response.results.map(result => ({ id: result.id, product: result.metadata, score: result.ranking_score, })), metadata: { recommendationToken: response.recommendationToken || '', outOfStockProducts: response.outOfStockProducts || [], missingProducts: response.missingProducts || [], }, }; } } exports.RetailClient = RetailClient;