Ingredients as many-to-many relations + VS Code/Antigravity launch.json
- 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)
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
/// <reference path="../pb_data/types.d.ts" />
|
||||
migrate(
|
||||
(app) => {
|
||||
// v4: ingredients become a many-to-many relation instead of the diet
|
||||
// select. Each ingredient carries its own icon (emoji) and diet class;
|
||||
// an item's veg/non-veg badge is derived from its ingredients. The menu
|
||||
// records are re-synced by scripts/update-menu.mjs.
|
||||
const ingredientsCollectionDef = () =>
|
||||
new Collection({
|
||||
name: 'ingredients',
|
||||
type: 'base',
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{ name: 'uid', type: 'text', required: true },
|
||||
{ name: 'name', type: 'text', required: true },
|
||||
{ name: 'icon', type: 'text' },
|
||||
{ name: 'diet_class', type: 'select', maxSelect: 1, values: ['veg', 'nonveg'], required: true },
|
||||
{ name: 'sort', type: 'number' },
|
||||
],
|
||||
indexes: ['CREATE UNIQUE INDEX idx_ingredients_uid ON ingredients (uid)'],
|
||||
})
|
||||
|
||||
// app.save() returns nil (falsy) on success, so don't test its return
|
||||
// value — resolve the saved collection by name instead. The lookup-first
|
||||
// pattern keeps the migration idempotent if a previous run created the
|
||||
// collection before failing.
|
||||
let ingredientsCollection
|
||||
try {
|
||||
ingredientsCollection = app.findCollectionByNameOrId('ingredients')
|
||||
} catch (e) {
|
||||
app.save(ingredientsCollectionDef())
|
||||
ingredientsCollection = app.findCollectionByNameOrId('ingredients')
|
||||
}
|
||||
|
||||
try {
|
||||
const oldCollection = app.findCollectionByNameOrId('menu_items')
|
||||
app.delete(oldCollection)
|
||||
} catch (e) {
|
||||
// collection may not exist on a fresh database — nothing to migrate
|
||||
}
|
||||
|
||||
const menuItems = new Collection({
|
||||
name: 'menu_items',
|
||||
type: 'base',
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{ name: 'uid', type: 'text', required: true },
|
||||
{ name: 'category', type: 'text', required: true },
|
||||
{ name: 'name', type: 'text', required: true },
|
||||
{ name: 'variant', type: 'text' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'price', type: 'number', required: true },
|
||||
{ name: 'ingredients', type: 'relation', collectionId: ingredientsCollection.id, cascadeDelete: false, maxSelect: 99 },
|
||||
{ name: 'pairing', type: 'text' },
|
||||
{ name: 'tag', type: 'text' },
|
||||
{ name: 'image', type: 'text' },
|
||||
{ name: 'sku', type: 'text' },
|
||||
{ name: 'available', type: 'bool' },
|
||||
{ name: 'tax_category', type: 'text' },
|
||||
{ name: 'sort', type: 'number' },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)',
|
||||
'CREATE UNIQUE INDEX idx_menu_items_sku ON menu_items (sku) WHERE sku != \'\'',
|
||||
],
|
||||
})
|
||||
return app.save(menuItems)
|
||||
},
|
||||
(app) => {
|
||||
// Revert to the v3 menu_items (diet select) and drop ingredients.
|
||||
try {
|
||||
const newCollection = app.findCollectionByNameOrId('menu_items')
|
||||
app.delete(newCollection)
|
||||
} catch (e) {
|
||||
// nothing to revert
|
||||
}
|
||||
|
||||
const menuItems = new Collection({
|
||||
name: 'menu_items',
|
||||
type: 'base',
|
||||
listRule: '',
|
||||
viewRule: '',
|
||||
createRule: null,
|
||||
updateRule: null,
|
||||
deleteRule: null,
|
||||
fields: [
|
||||
{ name: 'uid', type: 'text', required: true },
|
||||
{ name: 'category', type: 'text', required: true },
|
||||
{ name: 'name', type: 'text', required: true },
|
||||
{ name: 'variant', type: 'text' },
|
||||
{ name: 'description', type: 'text' },
|
||||
{ name: 'price', type: 'number', required: true },
|
||||
{ name: 'diet', type: 'select', maxSelect: 1, values: ['veg', 'nonveg', 'mixed'] },
|
||||
{ name: 'pairing', type: 'text' },
|
||||
{ name: 'tag', type: 'text' },
|
||||
{ name: 'image', type: 'text' },
|
||||
{ name: 'sku', type: 'text' },
|
||||
{ name: 'available', type: 'bool' },
|
||||
{ name: 'tax_category', type: 'text' },
|
||||
{ name: 'sort', type: 'number' },
|
||||
],
|
||||
indexes: [
|
||||
'CREATE UNIQUE INDEX idx_menu_items_uid ON menu_items (uid)',
|
||||
'CREATE UNIQUE INDEX idx_menu_items_sku ON menu_items (sku) WHERE sku != \'\'',
|
||||
],
|
||||
})
|
||||
app.save(menuItems)
|
||||
|
||||
try {
|
||||
const ingredients = app.findCollectionByNameOrId('ingredients')
|
||||
app.delete(ingredients)
|
||||
} catch (e) {
|
||||
// nothing to revert
|
||||
}
|
||||
return true
|
||||
}
|
||||
)
|
||||
Reference in New Issue
Block a user