Initial build: Great Bear microbrewery site
- TanStack Start (React 19, Tailwind 4) frontend recreated from Stitch design - PocketBase backend with schema migration + idempotent content seed - Single Docker image running both services (non-root, healthchecked) - deploy.sh to build & push to registry.tanshu.com - Ansible playbook (roles: app, caddy) deploying to www.greatbear.in with Caddy TLS entries for the site and the PocketBase admin
This commit is contained in:
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Idempotent content seeder.
|
||||
*
|
||||
* Reads content.json (same file the frontend bundles as offline fallback)
|
||||
* and creates any record that is missing in PocketBase, identified by its
|
||||
* `uid` field. Existing records are NEVER overwritten — after the first
|
||||
* seed, PocketBase owns the data and content is managed via the admin UI
|
||||
* (admin.greatbear.in).
|
||||
*
|
||||
* Required env: PB_SUPERUSER_EMAIL, PB_SUPERUSER_PASSWORD. Optional: PB_URL.
|
||||
*/
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
const here = dirname(fileURLToPath(import.meta.url))
|
||||
const PB_URL = process.env.PB_URL ?? 'http://127.0.0.1:8090'
|
||||
const EMAIL = process.env.PB_SUPERUSER_EMAIL
|
||||
const PASSWORD = process.env.PB_SUPERUSER_PASSWORD
|
||||
|
||||
if (!EMAIL || !PASSWORD) {
|
||||
console.error('[seed] PB_SUPERUSER_EMAIL/PB_SUPERUSER_PASSWORD not set — skipping seed')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const content = JSON.parse(readFileSync(join(here, 'content.json'), 'utf8'))
|
||||
|
||||
async function auth() {
|
||||
const res = await fetch(`${PB_URL}/api/collections/_superusers/auth-with-password`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ identity: EMAIL, password: PASSWORD }),
|
||||
})
|
||||
if (!res.ok) throw new Error(`superuser auth failed (HTTP ${res.status})`)
|
||||
const json = await res.json()
|
||||
return json.token
|
||||
}
|
||||
|
||||
async function ensureRecord(collection, token, item) {
|
||||
const filter = encodeURIComponent(`uid="${item.uid}"`)
|
||||
const found = await fetch(
|
||||
`${PB_URL}/api/collections/${collection}/records?filter=${filter}`,
|
||||
{ headers: { Authorization: token } },
|
||||
)
|
||||
if (!found.ok) throw new Error(`lookup failed for ${collection}/${item.uid} (HTTP ${found.status})`)
|
||||
const list = await found.json()
|
||||
if (list.items.length > 0) return 'exists'
|
||||
|
||||
const created = await fetch(`${PB_URL}/api/collections/${collection}/records`, {
|
||||
method: 'POST',
|
||||
headers: { Authorization: token, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(item),
|
||||
})
|
||||
if (!created.ok) {
|
||||
const body = await created.text()
|
||||
throw new Error(`create failed for ${collection}/${item.uid} (HTTP ${created.status}): ${body}`)
|
||||
}
|
||||
return 'created'
|
||||
}
|
||||
|
||||
const COLLECTIONS = [
|
||||
'settings',
|
||||
'stats',
|
||||
'brews',
|
||||
'experiences',
|
||||
'testimonials',
|
||||
'menu_categories',
|
||||
'menu_items',
|
||||
'pairings',
|
||||
]
|
||||
|
||||
try {
|
||||
const token = await auth()
|
||||
let created = 0
|
||||
let existing = 0
|
||||
for (const collection of COLLECTIONS) {
|
||||
const items = Array.isArray(content[collection])
|
||||
? content[collection]
|
||||
: content[collection]
|
||||
? [content[collection]]
|
||||
: []
|
||||
for (const item of items) {
|
||||
const result = await ensureRecord(collection, token, item)
|
||||
if (result === 'created') created++
|
||||
else existing++
|
||||
}
|
||||
}
|
||||
console.log(`[seed] done: ${created} created, ${existing} already present`)
|
||||
} catch (error) {
|
||||
console.error(`[seed] failed: ${error.message}`)
|
||||
process.exit(1)
|
||||
}
|
||||
Reference in New Issue
Block a user