Files
formulation/scripts/lib/site-projection.mjs
T

54 lines
8.6 KiB
JavaScript

import fs from "node:fs";
import path from "node:path";
const json = (value, fallback) => { try { return JSON.parse(value); } catch { return fallback; } };
export const titleCase = (input) => input.split(/\s+/).map((word) => { const index = word.search(/\p{L}/u); return index === -1 ? word : word.slice(0, index) + word[index].toLocaleUpperCase() + word.slice(index + 1); }).join(" ");
export function createSiteProjection(database) {
const units = database.prepare("SELECT * FROM units ORDER BY id").all().map((row) => ({ schema_version:2,id:row.id,name:row.name,symbol:row.symbol,dimension:row.dimension,system:row.system,...(row.base_unit_id?{base_conversion:{base_unit_id:row.base_unit_id,factor:row.factor,...(row.offset!=null?{offset:row.offset}:{})}}:{}) }));
const aliasQuery=database.prepare("SELECT name,kind FROM ingredient_aliases WHERE ingredient_id=? ORDER BY name");
const densityQuery=database.prepare("SELECT * FROM ingredient_density_measurements WHERE ingredient_id=? ORDER BY id");
const conversionQuery=database.prepare("SELECT * FROM ingredient_measure_conversions WHERE ingredient_id=? ORDER BY id");
const ingredientPrepQuery=database.prepare("SELECT action_id,yield_factor,notes FROM ingredient_prep_actions WHERE ingredient_id=? ORDER BY action_id");
const mappingIdsQuery=database.prepare("SELECT id,mapping_type FROM source_mappings WHERE subject_type='ingredient' AND subject_id=? AND status='reviewed' ORDER BY id");
const ingredients = database.prepare("SELECT * FROM ingredients ORDER BY id").all().map((row) => {
const mappings=mappingIdsQuery.all(row.id), source=json(row.source_json,"{}");
return { schema_version:row.schema_version,id:row.id,name:titleCase(row.name),...(row.description?{description:row.description}:{}),status:row.status,categories:json(row.categories_json,[]),tags:json(row.tags_json,[]),
aliases:aliasQuery.all(row.id),
density_measurements:densityQuery.all(row.id).map((value)=>({id:value.id,mass:{quantity:value.mass_quantity,unit_id:value.mass_unit_id},volume:{quantity:value.volume_quantity,unit_id:value.volume_unit_id},...(value.temperature_c!=null?{temperature_c:value.temperature_c}:{}),...(value.state?{state:value.state}:{}),source:json(value.source_json,{})})),
measure_conversions:conversionQuery.all(row.id).map((value)=>({id:value.id,from:{quantity:value.from_quantity,unit_id:value.from_unit_id},to:{quantity:value.to_quantity,unit_id:value.to_unit_id},...(value.state?{state:value.state}:{}),source:json(value.source_json,{})})),
prep_actions:ingredientPrepQuery.all(row.id).map((value)=>({action_id:value.action_id,yield_factor:value.yield_factor,...(value.notes?{notes:value.notes}:{})})),
nutrition_mapping_ids:mappings.filter((value)=>value.mapping_type==="nutrition").map((value)=>value.id),allergen_mapping_ids:mappings.filter((value)=>value.mapping_type==="allergen").map((value)=>value.id),
...(source.shelf_life?{shelf_life:source.shelf_life}:{}) };
});
const equipment = database.prepare("SELECT * FROM equipment ORDER BY id").all().map((row)=>({schema_version:2,id:row.id,name:row.name,...(row.category?{category:row.category}:{}),...(row.notes?{notes:row.notes}:{})}));
const prepActions = database.prepare("SELECT * FROM prep_actions ORDER BY id").all().map((row)=>({schema_version:2,id:row.id,name:row.name,action_type:row.action_type,...(row.default_yield_factor!=null?{default_yield_factor:row.default_yield_factor}:{}),...(row.notes?{notes:row.notes}:{})}));
const priceQuery=database.prepare("SELECT * FROM price_observations WHERE purchase_item_id=? ORDER BY effective_at,currency");
const purchaseItems = database.prepare("SELECT * FROM purchase_items ORDER BY id").all().map((row)=>({schema_version:2,id:row.id,ingredient_id:row.ingredient_id,name:row.name,...(row.brand?{brand:row.brand}:{}),...(row.supplier_id?{supplier_id:row.supplier_id}:{}),...(row.supplier_sku?{supplier_sku:row.supplier_sku}:{}),status:row.status,package:{quantity:row.package_quantity,unit_id:row.package_unit_id,units_per_case:row.units_per_case,usable_yield_factor:row.usable_yield_factor},prices:priceQuery.all(row.id).map((price)=>({amount:price.amount,currency:price.currency,effective_at:price.effective_at,source:json(price.source_json,{})}))}));
const sourceMappings = database.prepare("SELECT * FROM source_mappings ORDER BY id").all().map((row)=>({schema_version:2,id:row.id,subject:{type:row.subject_type,id:row.subject_id},mapping_type:row.mapping_type,status:row.status,source:json(row.source_json,{}),...(row.nutrition_json?{nutrition_per_100g:json(row.nutrition_json,{})}:{})}));
const componentQuery=database.prepare("SELECT * FROM recipe_components WHERE recipe_id=? ORDER BY position");
const itemQuery=database.prepare("SELECT * FROM recipe_items WHERE recipe_id=? AND component_id=? ORDER BY position");
const itemPrepQuery=database.prepare("SELECT action_id,yield_factor,notes FROM item_prep_actions WHERE recipe_id=? AND item_id=? ORDER BY position");
const stepQuery=database.prepare("SELECT * FROM recipe_steps WHERE recipe_id=? ORDER BY position");
const stepEquipmentQuery=database.prepare("SELECT equipment_id FROM step_equipment WHERE recipe_id=? AND step_id=? ORDER BY equipment_id");
const recipeEquipmentQuery=database.prepare("SELECT equipment_id FROM recipe_equipment WHERE recipe_id=? ORDER BY equipment_id");
const recipeConversionQuery=database.prepare("SELECT * FROM recipe_measure_conversions WHERE recipe_id=? ORDER BY id");
const recipes = database.prepare("SELECT * FROM recipes ORDER BY id").all().map((row)=>{
const source=json(row.source_json,{}), oldComponents=new Map((source.components??[]).map((value)=>[value.id,value])), oldSteps=new Map((source.steps??[]).map((value)=>[value.id,value]));
const components=componentQuery.all(row.id).map((component)=>{const old=oldComponents.get(component.id)??{};return {...old,id:component.id,name:component.name,notes:json(component.notes_json,[]),items:itemQuery.all(row.id,component.id).map((item)=>({id:item.id,reference:item.ingredient_id?{ingredient_id:item.ingredient_id}:{recipe_id:item.subrecipe_id},amount:{quantity:item.quantity,unit_id:item.unit_id},...(item.percentage!=null?{percentage:item.percentage}:{}),...(item.basis_member?{basis_member:true}:{}),...(item.optional?{optional:true}:{}),...(item.notes?{notes:item.notes}:{}),...(item.nutrition_retention_factor!==1?{nutrition_retention_factor:item.nutrition_retention_factor}:{}),...(itemPrepQuery.all(row.id,item.id).length?{prep:itemPrepQuery.all(row.id,item.id).map((prep)=>({action_id:prep.action_id,...(prep.yield_factor!=null?{yield_factor:prep.yield_factor}:{}),...(prep.notes?{notes:prep.notes}:{})}))}:{})}))};});
const steps=stepQuery.all(row.id).map((step,index)=>{const old=oldSteps.get(step.id)??{},equipment_ids=stepEquipmentQuery.all(row.id,step.id).map((value)=>value.equipment_id);return {...old,id:step.id,order:index+1,instruction:step.instruction,...(step.critical_control_point?{critical_control_point:true}:{}),...(equipment_ids.length?{equipment_ids}:{})};});
const equipment_ids=recipeEquipmentQuery.all(row.id).map((value)=>value.equipment_id);
source.measure_conversions=recipeConversionQuery.all(row.id).map((value)=>({id:value.id,from:{quantity:value.from_quantity,unit_id:value.from_unit_id},to:{quantity:value.to_quantity,unit_id:value.to_unit_id},...(value.notes?{notes:value.notes}:{}),source:json(value.source_json,{})}));
const scaling=row.scaling_mode?{mode:row.scaling_mode,...(row.scaling_basis_id?{basis_id:row.scaling_basis_id}:{}),...(row.scaling_basis_quantity!=null&&row.scaling_basis_unit_id?{basis_amount:{quantity:row.scaling_basis_quantity,unit_id:row.scaling_basis_unit_id}}:{})}:undefined;
return {...source,schema_version:row.schema_version,id:row.id,title:row.title,...(row.summary?{summary:row.summary}:{}),categories:json(row.categories_json,[]),tags:json(row.tags_json,[]),...(row.station?{station:row.station}:{}),...(row.auto_yield?{auto_yield:true}:{}),yield:{...(source.yield??{}),amount:{quantity:row.yield_quantity,unit_id:row.yield_unit_id},...(row.yield_servings!=null?{servings:row.yield_servings}:{}),...(row.yield_basis?{basis:row.yield_basis}:{})},...(scaling?{scaling}:{}),components,steps,notes:json(row.notes_json,[]),...(equipment_ids.length?{equipment_ids}:{})};
});
return { generated_at:new Date().toISOString(),units,ingredients,equipment,prepActions,purchaseItems,sourceMappings,recipes };
}
export function writeSiteProjection(database, target=path.resolve(process.cwd(),"generated","site-projection.json")) {
fs.mkdirSync(path.dirname(target),{recursive:true});
fs.writeFileSync(target,`${JSON.stringify(createSiteProjection(database))}\n`);
return target;
}