157 lines
4.6 KiB
TypeScript
157 lines
4.6 KiB
TypeScript
import { spawnSync } from "node:child_process"
|
|
import { config as loadEnv } from "dotenv"
|
|
import { ConvexHttpClient } from "convex/browser"
|
|
import { makeFunctionReference } from "convex/server"
|
|
import { scanManuals } from "../lib/manuals"
|
|
import { buildManualAssetUrl, buildThumbnailAssetUrl, getManualsAssetSource } from "../lib/manuals-storage"
|
|
import { businessConfig } from "../lib/seo-config"
|
|
import { getSiteDomain } from "../lib/site-config"
|
|
|
|
loadEnv({ path: ".env.local" })
|
|
loadEnv({ path: ".env.staging", override: false })
|
|
|
|
type ManualUpsertInput = {
|
|
filename: string
|
|
path: string
|
|
manufacturer: string
|
|
category: string
|
|
size?: number
|
|
lastModified?: number
|
|
searchTerms?: string[]
|
|
commonNames?: string[]
|
|
thumbnailUrl?: string
|
|
manualUrl?: string
|
|
hasParts?: boolean
|
|
assetSource?: string
|
|
sourcePath?: string
|
|
sourceSite?: string
|
|
sourceDomain?: string
|
|
siteVisibility?: string[]
|
|
importBatch?: string
|
|
}
|
|
|
|
const UPSERT_MANUALS = makeFunctionReference<"mutation">("manuals:upsertMany")
|
|
|
|
function parseArgs(argv: string[]) {
|
|
const limitFlagIndex = argv.indexOf("--limit")
|
|
const limit = limitFlagIndex >= 0 ? Number.parseInt(argv[limitFlagIndex + 1] || "", 10) : undefined
|
|
|
|
return {
|
|
dryRun: argv.includes("--dry-run"),
|
|
skipUpload: argv.includes("--skip-upload"),
|
|
limit: Number.isFinite(limit) && limit && limit > 0 ? limit : undefined,
|
|
}
|
|
}
|
|
|
|
function readConvexUrl() {
|
|
const convexUrl = process.env.NEXT_PUBLIC_CONVEX_URL || process.env.CONVEX_URL
|
|
if (!convexUrl) {
|
|
throw new Error("NEXT_PUBLIC_CONVEX_URL is required to sync manuals into Convex.")
|
|
}
|
|
|
|
return convexUrl
|
|
}
|
|
|
|
function shouldUploadAssets() {
|
|
const accessKey =
|
|
process.env.CLOUDFLARE_R2_ACCESS_KEY_ID ||
|
|
process.env.AWS_ACCESS_KEY_ID ||
|
|
process.env.AWS_ACCESS_KEY
|
|
const secret =
|
|
process.env.CLOUDFLARE_R2_SECRET_ACCESS_KEY ||
|
|
process.env.AWS_SECRET_ACCESS_KEY ||
|
|
process.env.AWS_SECRET_KEY
|
|
|
|
return Boolean(accessKey && secret)
|
|
}
|
|
|
|
function runAssetUpload() {
|
|
const result = spawnSync("node", ["scripts/upload-to-r2.js", "--type", "all", "--incremental"], {
|
|
cwd: process.cwd(),
|
|
stdio: "inherit",
|
|
})
|
|
|
|
if ((result.status ?? 1) !== 0) {
|
|
throw new Error("R2 upload step failed.")
|
|
}
|
|
}
|
|
|
|
function normalizeManualForConvex(
|
|
manual: Awaited<ReturnType<typeof scanManuals>>[number],
|
|
importBatch: string,
|
|
) {
|
|
const siteDomain = getSiteDomain()
|
|
const manualUrl = buildManualAssetUrl(manual.path)
|
|
const thumbnailUrl = manual.thumbnailUrl ? buildThumbnailAssetUrl(manual.thumbnailUrl) : undefined
|
|
|
|
return {
|
|
filename: manual.filename,
|
|
path: manual.path,
|
|
manufacturer: manual.manufacturer,
|
|
category: manual.category,
|
|
size: manual.size,
|
|
lastModified: manual.lastModified ? manual.lastModified.getTime() : undefined,
|
|
searchTerms: manual.searchTerms,
|
|
commonNames: manual.commonNames,
|
|
thumbnailUrl,
|
|
manualUrl,
|
|
hasParts: Boolean(manual.searchTerms?.some((term) => term.toLowerCase().includes("parts"))),
|
|
assetSource: getManualsAssetSource(),
|
|
sourcePath: manual.path,
|
|
sourceSite: businessConfig.name,
|
|
sourceDomain: siteDomain,
|
|
siteVisibility: [siteDomain],
|
|
importBatch,
|
|
} satisfies ManualUpsertInput
|
|
}
|
|
|
|
async function main() {
|
|
const args = parseArgs(process.argv.slice(2))
|
|
|
|
if (!args.skipUpload && shouldUploadAssets()) {
|
|
console.log("[manuals-sync] uploading new manuals and thumbnails to object storage")
|
|
runAssetUpload()
|
|
} else if (!args.skipUpload) {
|
|
console.log("[manuals-sync] skipping asset upload because Cloudflare R2 credentials are not configured")
|
|
}
|
|
|
|
const manuals = await scanManuals()
|
|
const selectedManuals = args.limit ? manuals.slice(0, args.limit) : manuals
|
|
const importBatch = new Date().toISOString()
|
|
const payload = selectedManuals.map((manual) => normalizeManualForConvex(manual, importBatch))
|
|
|
|
console.log("[manuals-sync] scanned manuals", {
|
|
total: manuals.length,
|
|
selected: selectedManuals.length,
|
|
assetSource: getManualsAssetSource(),
|
|
})
|
|
|
|
if (args.dryRun) {
|
|
console.log("[manuals-sync] dry run complete")
|
|
return
|
|
}
|
|
|
|
const convex = new ConvexHttpClient(readConvexUrl())
|
|
const batchSize = 100
|
|
let synced = 0
|
|
|
|
for (let index = 0; index < payload.length; index += batchSize) {
|
|
const slice = payload.slice(index, index + batchSize)
|
|
await convex.mutation(UPSERT_MANUALS, { manuals: slice })
|
|
synced += slice.length
|
|
console.log("[manuals-sync] upserted batch", {
|
|
synced,
|
|
total: payload.length,
|
|
})
|
|
}
|
|
|
|
console.log("[manuals-sync] completed", {
|
|
synced,
|
|
importBatch,
|
|
})
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error("[manuals-sync] failed", error)
|
|
process.exit(1)
|
|
})
|