161 lines
4.6 KiB
TypeScript

import { buildCacheKey, getCached, setCached } from "@/server/lib/r2-cache";
import { normalizeBacklinksTarget } from "@/server/lib/dataforseo";
import {
normalizeBacklinksSpamFilterOptions,
type BacklinksLookupInput,
type BacklinksSpamFilterOptions,
} from "@/types/schemas/backlinks";
import {
profileBacklinksOverview,
profileBacklinksRowsPage,
profileReferringDomainsPage,
profileTopPagesPage,
type BacklinksCache,
type BacklinksRowsPageServiceInput,
type ReferringDomainsPageServiceInput,
type TopPagesPageServiceInput,
} from "@/server/features/backlinks/services/backlinksServiceData";
import type { BillingCustomerContext } from "@/server/billing/subscription";
import type { CreditFeature } from "@/shared/billing-credit-features";
const defaultCache: BacklinksCache = {
get: getCached,
set: setCached,
};
type BacklinksPageCacheInput = {
target: string;
scope?: BacklinksLookupInput["scope"];
page: number;
pageSize: number;
sortField: string;
sortOrder: string;
filters: Record<string, unknown>;
/** Backlinks rows only: DataForSEO result grouping. */
mode?: string;
};
function createBacklinksService(cache: BacklinksCache = defaultCache) {
return {
async profileOverview(
input: BacklinksLookupInput,
billingCustomer: BillingCustomerContext,
// Lets a caller (e.g. onboarding) attribute the spend to its own credit
// feature. Applied to the DataForSEO calls, not the cache key, so cached
// results stay shared across callers.
creditFeature?: CreditFeature,
) {
const cacheKey = await buildCacheKey("backlinks:overview", {
...buildTargetCacheInput(input, billingCustomer),
});
return profileBacklinksOverview(
cache,
cacheKey,
input,
billingCustomer,
creditFeature,
);
},
async profileBacklinksPage(
input: BacklinksRowsPageServiceInput,
billingCustomer: BillingCustomerContext,
options?: BacklinksSpamFilterOptions,
) {
const cacheKey = await buildPageCacheKey(
"backlinks:rows-page",
input,
billingCustomer,
options,
);
return profileBacklinksRowsPage(
cache,
cacheKey,
input,
billingCustomer,
options,
);
},
async profileReferringDomainsPage(
input: ReferringDomainsPageServiceInput,
billingCustomer: BillingCustomerContext,
options?: BacklinksSpamFilterOptions,
) {
const cacheKey = await buildPageCacheKey(
"backlinks:referring-domains-page",
input,
billingCustomer,
options,
);
return profileReferringDomainsPage(
cache,
cacheKey,
input,
billingCustomer,
options,
);
},
async profileTopPagesPage(
input: TopPagesPageServiceInput,
billingCustomer: BillingCustomerContext,
) {
const cacheKey = await buildPageCacheKey(
"backlinks:top-pages-page",
input,
billingCustomer,
);
return profileTopPagesPage(cache, cacheKey, input, billingCustomer);
},
} as const;
}
function buildTargetCacheInput(
input: BacklinksLookupInput,
billingCustomer: BillingCustomerContext,
) {
const normalizedTarget = normalizeBacklinksTarget(input.target, {
scope: input.scope,
});
return {
organizationId: billingCustomer.organizationId,
target: normalizedTarget.apiTarget,
scope: normalizedTarget.scope,
// Subfolder scope keeps the hostname as the API target, so the path must
// separate cache entries.
path: normalizedTarget.path,
// Same hostname, different result set — and keeping it in the key retires
// entries written before scopes could exclude subdomains.
includeSubdomains: normalizedTarget.includeSubdomains,
};
}
async function buildPageCacheKey(
prefix: string,
input: BacklinksPageCacheInput,
billingCustomer: BillingCustomerContext,
options?: BacklinksSpamFilterOptions,
): Promise<string> {
const spamFilterOptions = normalizeBacklinksSpamFilterOptions(options);
return buildCacheKey(prefix, {
...buildTargetCacheInput(input, billingCustomer),
page: input.page,
pageSize: input.pageSize,
sortField: input.sortField,
sortOrder: input.sortOrder,
filters: input.filters,
...(input.mode ? { mode: input.mode } : {}),
hideSpam: String(spamFilterOptions.hideSpam),
...(spamFilterOptions.hideSpam
? { spamThreshold: String(spamFilterOptions.spamThreshold) }
: {}),
});
}
export const BacklinksService = createBacklinksService();
export { createBacklinksService };