google-retail-api-client
Version:
Official TypeScript client for Google Retail API
224 lines (223 loc) • 9.02 kB
JavaScript
;
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;