← back to Open Seo
scripts/backlinks-cost-profile.ts
171 lines
import process from "node:process";
import { createBacklinksService } from "@/server/features/backlinks/services/BacklinksService";
import type { BillingCustomerContext } from "@/server/billing/subscription";
import type {
BacklinksLookupInput,
BacklinksTargetScope,
} from "@/types/schemas/backlinks";
import { loadLocalEnv, parseArgs } from "./cli-utils";
loadLocalEnv();
const args = parseArgs(process.argv.slice(2));
const inMemoryCache = new Map<string, string>();
const service = createBacklinksService({
async get(key) {
const raw = inMemoryCache.get(key);
return raw ? parseCachedValue(raw) : null;
},
async set(key, data) {
inMemoryCache.set(key, JSON.stringify(data));
},
});
await main();
async function main() {
if (process.env.CI === "true" && args.allowCi !== "true") {
printUsageAndExit(
"Refusing to run live billing checks in CI without --allowCi=true.",
);
}
if (args.confirmLive !== "true") {
printUsageAndExit(
"This command makes live, billable DataForSEO requests. Re-run with --confirmLive=true.",
);
}
const input = buildInput(args);
const billingCustomer = buildBillingCustomer(args);
const repeat = parsePositiveInteger(args.repeat, 1);
const includeTabs = parseBoolean(args.includeTabs, true);
const runs = [];
const pageInput = {
...input,
page: 1,
pageSize: 100,
sortOrder: "desc",
} as const;
for (let index = 0; index < repeat; index += 1) {
const overview = await service.profileOverview(input, billingCustomer);
const rows = includeTabs
? await service.profileBacklinksPage(
{ ...pageInput, sortField: "rank", filters: {}, mode: "as_is" },
billingCustomer,
)
: null;
const domains = includeTabs
? await service.profileReferringDomainsPage(
{ ...pageInput, sortField: "backlinks", filters: {} },
billingCustomer,
)
: null;
const pages = includeTabs
? await service.profileTopPagesPage(
{ ...pageInput, sortField: "backlinks", filters: {} },
billingCustomer,
)
: null;
runs.push({
run: index + 1,
overview: {
trendRows: overview.overview.trends.length,
newLostRows: overview.overview.newLostTrends.length,
},
backlinksTab: rows
? {
rows: rows.rows.length,
}
: null,
domainsTab: domains
? {
rows: domains.rows.length,
}
: null,
pagesTab: pages
? {
rows: pages.rows.length,
}
: null,
});
}
console.log(
JSON.stringify(
{
input,
repeat,
includeTabs,
runs,
},
null,
2,
),
);
}
function buildInput(cliArgs: Record<string, string>): BacklinksLookupInput {
const target = cliArgs.target;
const scope = parseScope(cliArgs.scope);
if (!target) {
printUsageAndExit("Missing target.");
}
if (!process.env.DATAFORSEO_API_KEY) {
printUsageAndExit("Missing DATAFORSEO_API_KEY.");
}
return {
target,
scope,
};
}
function buildBillingCustomer(
cliArgs: Record<string, string>,
): BillingCustomerContext {
return {
organizationId: cliArgs.organizationId ?? "local",
userId: cliArgs.userId ?? "local-user",
userEmail: cliArgs.userEmail ?? "local@example.com",
};
}
function parseBoolean(value: string | undefined, fallback: boolean) {
if (value == null) return fallback;
return value === "true";
}
function parseScope(
value: string | undefined,
): BacklinksTargetScope | undefined {
if (!value) return undefined;
if (value === "domain" || value === "page") return value;
printUsageAndExit(`Invalid scope: ${value}. Expected domain or page.`);
}
function parsePositiveInteger(value: string | undefined, fallback: number) {
if (!value) return fallback;
const parsed = Number.parseInt(value, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
}
function printUsageAndExit(message: string): never {
console.error(message);
console.error(
"Usage: pnpm billing:backlinks --target=example.com --confirmLive=true [--scope=domain|page] [--repeat=1] [--includeTabs=true|false] [--allowCi=true]",
);
process.exit(1);
}
function parseCachedValue(raw: string): unknown {
try {
return JSON.parse(raw) as unknown;
} catch {
return null;
}
}