- new ingredients collection (uid, name, emoji icon, diet_class) synced by update-menu.mjs; menu_items.ingredients is an M2M relation — the script maps data-file uids to fresh PB record ids - diet select removed: the green/red FSSAI badge is derived (non-veg when any linked ingredient is non-veg); 164 items tagged (11 ingredients) - menu cards render ingredient chips (icon + name) under the description - migration 1788500200_menu_ingredients.js (idempotent create — app.save returns nil on success in the JSVM, so don't test its return value) - .vscode/launch.json: PocketBase + Vite dev, build+preview, menu sync, full-stack compound (dev PB on :8091 — 8090 is taken on this host)
131 lines
4.5 KiB
JavaScript
Executable File
131 lines
4.5 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* Menu sync — replaces the PocketBase menu with app/src/data/menu-data.mjs.
|
|
*
|
|
* Deletes every record in menu_categories / ingredients / menu_items and
|
|
* inserts the current food menu from code. Item ingredient uids are mapped
|
|
* to the freshly created ingredient record ids. Nothing else is touched.
|
|
*
|
|
* Works against any PocketBase instance (local dev or production):
|
|
*
|
|
* # local dev
|
|
* PB_URL=http://127.0.0.1:8090 PB_SUPERUSER_EMAIL=dev@greatbear.in \
|
|
* PB_SUPERUSER_PASSWORD=devpass12345 node scripts/update-menu.mjs
|
|
*
|
|
* # production (admin domain serves the PocketBase API)
|
|
* PB_URL=https://admin.greatbear.in PB_SUPERUSER_EMAIL=admin@greatbear.in \
|
|
* PB_SUPERUSER_PASSWORD=... node scripts/update-menu.mjs
|
|
*/
|
|
import { dirname, join } from 'node:path'
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
const here = dirname(fileURLToPath(import.meta.url))
|
|
const { INGREDIENTS, MENU_CATEGORIES, MENU_ITEMS } = await import(
|
|
new URL(`file://${join(here, '../app/src/data/menu-data.mjs')}`)
|
|
)
|
|
|
|
const PB_URL = (process.env.PB_URL ?? 'http://127.0.0.1:8090').replace(/\/$/, '')
|
|
const EMAIL = process.env.PB_SUPERUSER_EMAIL
|
|
const PASSWORD = process.env.PB_SUPERUSER_PASSWORD
|
|
|
|
if (!EMAIL || !PASSWORD) {
|
|
console.error(
|
|
'[menu] PB_SUPERUSER_EMAIL and PB_SUPERUSER_PASSWORD are required\n' +
|
|
' e.g. PB_URL=https://admin.greatbear.in PB_SUPERUSER_EMAIL=admin@greatbear.in \\\n' +
|
|
' PB_SUPERUSER_PASSWORD=... node scripts/update-menu.mjs',
|
|
)
|
|
process.exit(1)
|
|
}
|
|
|
|
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 against ${PB_URL} (HTTP ${res.status})`)
|
|
const json = await res.json()
|
|
return json.token
|
|
}
|
|
|
|
async function listAll(collection, token) {
|
|
const records = []
|
|
let page = 1
|
|
for (;;) {
|
|
const res = await fetch(
|
|
`${PB_URL}/api/collections/${collection}/records?sort=sort&perPage=200&page=${page}`,
|
|
{ headers: { Authorization: token } },
|
|
)
|
|
if (!res.ok) throw new Error(`list ${collection} failed (HTTP ${res.status})`)
|
|
const json = await res.json()
|
|
records.push(...json.items)
|
|
if (json.items.length < json.perPage || page >= json.totalPages) break
|
|
page += 1
|
|
}
|
|
return records
|
|
}
|
|
|
|
async function deleteRecord(collection, token, id) {
|
|
const res = await fetch(`${PB_URL}/api/collections/${collection}/records/${id}`, {
|
|
method: 'DELETE',
|
|
headers: { Authorization: token },
|
|
})
|
|
if (!res.ok && res.status !== 404) {
|
|
throw new Error(`delete ${collection}/${id} failed (HTTP ${res.status})`)
|
|
}
|
|
}
|
|
|
|
async function createRecord(collection, token, body) {
|
|
const res = await fetch(`${PB_URL}/api/collections/${collection}/records`, {
|
|
method: 'POST',
|
|
headers: { Authorization: token, 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!res.ok) {
|
|
const text = await res.text()
|
|
throw new Error(`create ${collection}/${body.uid} failed (HTTP ${res.status}): ${text}`)
|
|
}
|
|
return res.json()
|
|
}
|
|
|
|
try {
|
|
const token = await auth()
|
|
console.log(`[menu] authenticated against ${PB_URL}`)
|
|
|
|
async function replaceCollection(collection, entries) {
|
|
const existing = await listAll(collection, token)
|
|
for (const record of existing) {
|
|
await deleteRecord(collection, token, record.id)
|
|
}
|
|
const idsByUid = new Map()
|
|
for (const entry of entries) {
|
|
const created = await createRecord(collection, token, entry)
|
|
idsByUid.set(entry.uid, created.id)
|
|
}
|
|
console.log(`[menu] ${collection}: -${existing.length} old, +${entries.length} new`)
|
|
return idsByUid
|
|
}
|
|
|
|
// Ingredients first — menu items reference their PocketBase record ids.
|
|
await replaceCollection('menu_categories', MENU_CATEGORIES)
|
|
const ingredientIds = await replaceCollection('ingredients', INGREDIENTS)
|
|
|
|
const items = MENU_ITEMS.map((entry) => {
|
|
const { ingredients, ...rest } = entry
|
|
return {
|
|
...rest,
|
|
ingredients: (ingredients ?? []).map((uid) => {
|
|
const id = ingredientIds.get(uid)
|
|
if (!id) throw new Error(`menu item ${entry.uid} references unknown ingredient '${uid}'`)
|
|
return id
|
|
}),
|
|
}
|
|
})
|
|
await replaceCollection('menu_items', items)
|
|
|
|
console.log('[menu] done — the live menu now matches app/src/data/menu-data.mjs')
|
|
} catch (error) {
|
|
console.error(`[menu] failed: ${error.message}`)
|
|
process.exit(1)
|
|
}
|