helper.ts•6.28 kB
import axios from "axios";
import { PlacesTextSearchParams } from "./schema.js";
import { z } from "zod";
// Define the type from the schema
type PlacesTextSearchParamsType = z.infer<typeof PlacesTextSearchParams>;
export class HttpError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "HttpError";
}
}
export class GooglePlacesHttpError extends Error {
constructor(public statusCode: number, message: string) {
super(message);
this.name = "GooglePlacesHttpError";
}
}
// Define optimized default fields - focused on essential information with review summaries
const DEFAULT_FIELDS = [
// Basic identification
"places.id",
"places.displayName",
// Location and address
"places.formattedAddress",
"places.location",
"places.googleMapsUri",
// Business information
"places.businessStatus",
"places.primaryType",
"places.primaryTypeDisplayName",
"places.types",
"places.photos",
// Ratings and contact
"places.rating",
"places.userRatingCount",
"places.priceLevel",
"places.priceRange",
"places.internationalPhoneNumber",
"places.websiteUri",
// Hours
"places.regularOpeningHours",
// Service capabilities
"places.delivery",
"places.dineIn",
"places.takeout",
"places.reservable",
// Essential food services
"places.servesVegetarianFood",
"places.servesBreakfast",
"places.servesLunch",
"places.servesDinner",
// Payment and accessibility
"places.paymentOptions",
"places.accessibilityOptions",
// Review summary (more concise than full reviews)
"places.reviewSummary"
].join(",");
export async function performPlacesTextSearch(
options: PlacesTextSearchParamsType,
fields?: string[]
) {
const url = new URL("https://places.googleapis.com/v1/places:searchText");
// Add region code as query parameter if provided
if (options.regionCode) {
url.searchParams.append("regionCode", options.regionCode);
}
const apiKey = process.env.GOOGLE_MAPS_API_KEY;
if (!apiKey) {
throw new GooglePlacesHttpError(500, "Google Maps API key not configured. Please set GOOGLE_MAPS_API_KEY environment variable.");
}
// Build request headers
const headers: Record<string, string> = {
"Content-Type": "application/json",
"X-Goog-Api-Key": apiKey,
};
// Add field mask if fields are specified, otherwise use optimized default fields
const fieldMask = Array.isArray(fields) && fields.length > 0
? fields.join(",")
: DEFAULT_FIELDS;
if (fieldMask) {
headers["X-Goog-FieldMask"] = fieldMask;
}
// Prepare request body with default pageSize of 5
const { fields: _fields, regionCode: _regionCode, ...requestBody } = options;
// Set default pageSize to 5 if not specified
if (!requestBody.pageSize) {
requestBody.pageSize = 5;
}
// Log request details in development mode
if (process.env.NODE_ENV === 'development') {
console.error("Making Google Places API request:", {
url: url.toString(),
body: JSON.stringify(requestBody, null, 2),
fieldMask: fieldMask.length > 100 ? fieldMask.substring(0, 100) + "..." : fieldMask
});
}
try {
const response = await axios.post(url.toString(), requestBody, {
headers,
timeout: 15000, // Increased timeout to 15 seconds
validateStatus: (status) => status < 500, // Don't throw on 4xx errors, handle them gracefully
});
if (process.env.NODE_ENV === 'development') {
console.error("Google Places API response status:", response.status);
console.error("Response data preview:", JSON.stringify(response.data, null, 2).substring(0, 500) + "...");
}
// Handle successful responses
if (response.status >= 200 && response.status < 300) {
const data = response.data;
// Post-process data to prevent truncation
if (data && data.places) {
data.places.forEach((place: any) => {
// Limit photos to maximum 3 to reduce data size
if (place.photos && place.photos.length > 3) {
place.photos = place.photos.slice(0, 3);
}
// Remove verbose fields that might cause truncation
delete place.contextualContents;
delete place.currentOpeningHours; // Keep only regularOpeningHours if present
});
}
return data;
}
// Handle 4xx errors with specific messages
const errorData = response.data;
let message = "Google Places API error";
if (errorData?.error?.message) {
message = errorData.error.message;
} else if (errorData?.error) {
message = JSON.stringify(errorData.error);
}
throw new GooglePlacesHttpError(response.status, message);
} catch (error) {
// Handle network errors and other exceptions
if (error instanceof GooglePlacesHttpError) {
throw error; // Re-throw our custom errors
}
if (axios.isAxiosError(error)) {
const statusCode = error.response?.status || 500;
const errorData = error.response?.data;
// Try to extract a meaningful error message
let message = "Google Places API error";
if (errorData?.error?.message) {
message = errorData.error.message;
} else if (errorData?.error) {
message = JSON.stringify(errorData.error);
} else if (error.message) {
message = error.message;
}
// Log detailed error information in development
if (process.env.NODE_ENV === 'development') {
console.error("API Error Details:", {
status: statusCode,
statusText: error.response?.statusText,
data: errorData,
headers: error.response?.headers,
requestConfig: {
url: error.config?.url,
method: error.config?.method,
headers: error.config?.headers
}
});
}
throw new GooglePlacesHttpError(statusCode, message);
}
// Handle other types of errors (network, timeout, etc.)
const message = error instanceof Error
? `Network error: ${error.message}`
: "Unknown error occurred while calling Google Places API";
throw new GooglePlacesHttpError(500, message);
}
}