95 lines
3.1 KiB
TypeScript
95 lines
3.1 KiB
TypeScript
/**
|
|
* 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)
|
|
})
|