/// 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 } )