Closer/functions/scripts/seedDateIdeas.ts

95 lines
3.1 KiB
TypeScript
Raw Normal View History

/**
* Seeds the top-level `date_ideas` collection from date_ideas_seed.json.
*
* A LOCAL admin script NOT a deployed callable (a deployed admin-write endpoint is avoidable
* attack surface). Run with admin credentials; idempotent by document id, so re-running updates in
* place rather than duplicating. Sponsored/seasonal ideas are edited here and pushed without an app
* release; the Android app falls back to its shipped static seed when the collection is empty.
*
* Usage:
* export GOOGLE_APPLICATION_CREDENTIALS=~/.config/closer/closer-admin-sa.json
* npx ts-node functions/scripts/seedDateIdeas.ts # writes
* npx ts-node functions/scripts/seedDateIdeas.ts --dry-run # prints what would change
*/
import * as admin from 'firebase-admin'
import * as fs from 'fs'
import * as path from 'path'
interface SeedIdea {
id: string
title: string
description?: string
category?: string
estimatedDuration?: string
estimatedCost?: string
isPremium?: boolean
sponsored?: boolean
sponsorName?: string
externalUrl?: string
}
const DRY_RUN = process.argv.includes('--dry-run')
function loadSeed(): SeedIdea[] {
const file = path.join(__dirname, 'date_ideas_seed.json')
const ideas = JSON.parse(fs.readFileSync(file, 'utf8')) as SeedIdea[]
const ids = new Set<string>()
for (const idea of ideas) {
if (!idea.id || !idea.title) throw new Error(`seed entry missing id/title: ${JSON.stringify(idea)}`)
if (ids.has(idea.id)) throw new Error(`duplicate seed id: ${idea.id}`)
ids.add(idea.id)
if (idea.externalUrl && !idea.externalUrl.startsWith('https://')) {
throw new Error(`externalUrl must be https: ${idea.id}`)
}
}
return ideas
}
function toDoc(idea: SeedIdea): Record<string, unknown> {
const doc: Record<string, unknown> = {
title: idea.title,
description: idea.description ?? '',
category: idea.category ?? '',
estimatedDuration: idea.estimatedDuration ?? '',
estimatedCost: idea.estimatedCost ?? 'free',
isPremium: idea.isPremium ?? false,
sponsored: idea.sponsored ?? false,
createdAt: Date.now(),
}
if (idea.sponsorName) doc.sponsorName = idea.sponsorName
if (idea.externalUrl) doc.externalUrl = idea.externalUrl
return doc
}
async function main() {
const ideas = loadSeed()
console.log(`Loaded ${ideas.length} date ideas (${ideas.filter((i) => i.sponsored).length} sponsored).`)
if (DRY_RUN) {
for (const idea of ideas) console.log(` would upsert date_ideas/${idea.id}`)
console.log('Dry run — nothing written.')
return
}
if (!admin.apps.length) admin.initializeApp()
const db = admin.firestore()
const col = db.collection('date_ideas')
// Batch upsert (merge) by id — idempotent.
let batch = db.batch()
let n = 0
for (const idea of ideas) {
batch.set(col.doc(idea.id), toDoc(idea), { merge: true })
if (++n % 400 === 0) {
await batch.commit()
batch = db.batch()
}
}
await batch.commit()
console.log(`Seeded ${ideas.length} ideas into date_ideas.`)
}
main().catch((err) => {
console.error(err)
process.exit(1)
})