import { z } from "zod"; import type { BillingCustomerContext } from "@/server/billing/subscription"; import { createDataforseoClient } from "@/server/lib/dataforseo"; import { buildCacheKey, getCached, setCached } from "@/server/lib/r2-cache"; import { normalizeDomainInput } from "@/server/lib/domainUtils"; import { mapKeywordItem } from "@/server/features/domain/services/domainKeywordMapper"; import { buildKeywordFilters, buildOrderBy, type DomainKeywordsSortMode, type DomainKeywordsSortOrder, } from "@/server/features/domain/services/domainKeywordFilters"; import type { DomainKeywordsFilters } from "@/types/schemas/domain"; const DOMAIN_KEYWORDS_PAGE_TTL_SECONDS = 12 * 60 * 60; const domainKeywordsPageResultSchema = z.object({ domain: z.string(), page: z.number(), pageSize: z.number(), totalCount: z.number().nullable(), hasMore: z.boolean(), keywords: z.array( z.object({ keyword: z.string(), position: z.number().nullable(), searchVolume: z.number().nullable(), traffic: z.number().nullable(), cpc: z.number().nullable(), url: z.string().nullable(), relativeUrl: z.string().nullable(), keywordDifficulty: z.number().nullable(), }), ), fetchedAt: z.string(), }); type DomainKeywordsPageResult = z.infer; export async function getKeywordsPage( input: { projectId: string; domain: string; includeSubdomains: boolean; locationCode: number; languageCode: string; page: number; pageSize: number; sortMode: DomainKeywordsSortMode; sortOrder: DomainKeywordsSortOrder; filters: DomainKeywordsFilters; search?: string; }, billingCustomer: BillingCustomerContext, ): Promise { const domain = normalizeDomainInput(input.domain, input.includeSubdomains); const offset = (input.page - 1) * input.pageSize; const orderBy = buildOrderBy(input.sortMode, input.sortOrder); const filters = buildKeywordFilters(input.filters, input.search); const cacheKey = await buildCacheKey("domain:keywords-page", { organizationId: billingCustomer.organizationId, projectId: input.projectId, domain, includeSubdomains: input.includeSubdomains, locationCode: input.locationCode, languageCode: input.languageCode, page: input.page, pageSize: input.pageSize, sortMode: input.sortMode, sortOrder: input.sortOrder, filters: input.filters, search: input.search, }); const cachedRaw = await getCached(cacheKey); const cached = domainKeywordsPageResultSchema.safeParse(cachedRaw); if (cached.success) { return cached.data; } const dataforseo = createDataforseoClient(billingCustomer); const response = await dataforseo.domain.rankedKeywords({ target: domain, locationCode: input.locationCode, languageCode: input.languageCode, limit: input.pageSize, offset, orderBy, filters: filters.length > 0 ? filters : undefined, }); const keywords = response.items .map((item) => mapKeywordItem(item)) .filter( (item): item is NonNullable> => item != null, ); const totalCount = response.totalCount; const hasMore = totalCount != null ? offset + keywords.length < totalCount : keywords.length === input.pageSize; const result: DomainKeywordsPageResult = { domain, page: input.page, pageSize: input.pageSize, totalCount, hasMore, keywords, fetchedAt: new Date().toISOString(), }; void setCached(cacheKey, result, DOMAIN_KEYWORDS_PAGE_TTL_SECONDS).catch( (error) => { console.error("domain.keywords-page.cache-write failed:", error); }, ); return result; }