90 lines
3.1 KiB
TypeScript

import { z } from "zod";
import {
BusinessDataBusinessListingsSearchLiveRequestInfo,
BusinessDataGoogleQuestionsAndAnswersLiveRequestInfo,
type BusinessDataBusinessListingsSearchLiveItem,
} from "dataforseo-client";
import { businessDataApi } from "@/server/lib/dataforseo/core";
import {
assertOk,
buildTaskBilling,
type DataforseoApiResponse,
} from "@/server/lib/dataforseo/envelope";
type BusinessListingItem = BusinessDataBusinessListingsSearchLiveItem;
export async function fetchBusinessListingsSearch(input: {
categories?: string[];
title?: string;
locationCoordinate: string;
orderBy?: string[];
limit: number;
}): Promise<DataforseoApiResponse<BusinessListingItem[]>> {
const response = await businessDataApi().businessListingsSearchLive([
new BusinessDataBusinessListingsSearchLiveRequestInfo({
categories: input.categories,
title: input.title,
location_coordinate: input.locationCoordinate,
order_by: input.orderBy,
limit: input.limit,
}),
]);
// "No Search Results" (40501) is a valid empty result for obscure
// businesses/keywords — DataForSEO still charges for it, so treat it as an
// empty success instead of surfacing a charged-task error to the user.
const task = assertOk(response, { treatNoResultsAsEmpty: true });
return {
data: task.result?.[0]?.items ?? [],
billing: buildTaskBilling(task),
};
}
// Q&A results carry both answered (`items`) and unanswered
// (`items_without_answers`) rows; the SDK types this result as `any`, so we
// validate a generic record shape and flatten both.
const questionsResultSchema = z
.object({
items: z.array(z.record(z.string(), z.unknown())).nullable().optional(),
items_without_answers: z
.array(z.record(z.string(), z.unknown()))
.nullable()
.optional(),
})
.passthrough();
function combinedQuestionItems(results: unknown): Record<string, unknown>[] {
const list = Array.isArray(results) ? results : [];
return list.flatMap((result) => {
const parsed = questionsResultSchema.safeParse(result ?? {});
if (!parsed.success) return [];
return [
...(parsed.data.items ?? []),
...(parsed.data.items_without_answers ?? []),
];
});
}
export async function fetchQuestionsAnswers(input: {
keyword: string;
locationCoordinate: string;
languageCode: string;
depth: number;
}): Promise<DataforseoApiResponse<Record<string, unknown>[]>> {
const response = await businessDataApi().googleQuestionsAndAnswersLive([
new BusinessDataGoogleQuestionsAndAnswersLiveRequestInfo({
keyword: input.keyword,
location_coordinate: input.locationCoordinate,
language_code: input.languageCode,
depth: input.depth,
}),
]);
// "No Search Results" (40501) is a valid empty result for obscure
// businesses/keywords — DataForSEO still charges for it, so treat it as an
// empty success instead of surfacing a charged-task error to the user.
const task = assertOk(response, { treatNoResultsAsEmpty: true });
return {
data: combinedQuestionItems(task.result),
billing: buildTaskBilling(task),
};
}