096d6e2868
CI / update (push) Successful in 3m31s
Replace season: number[] (months 1-12) on Recipe with seasonRanges, a list of date ranges where each endpoint is either a fixed MM-DD or a movable liturgical anchor (Easter, Ash Wednesday, Palm Sunday, Pentecost, Advent I) plus a day offset. The old month list couldn't express liturgical seasons whose boundaries shift each year (Advent, Lent, Easter Octave, Christmas Octave) nor sub-month windows. The shared evaluator resolves anchors against [Y-1, Y, Y+1] so spans that wrap the calendar year boundary (e.g. christmas + 0 to christmas + 7) match correctly on both sides. SeasonSelect was rewritten as a controlled bind:ranges editor with a fixed/liturgical kind toggle, anchor + offset inputs, per-row resolved-this-year preview, and preset chips. Run the one-time migration before deploying: pnpm exec vite-node scripts/migrate-season-to-ranges.ts It coalesces contiguous month runs into single fixed ranges and merges Dec/Jan wrap into one wrapping range; the new code does not read the legacy season field, so order matters.
228 lines
8.2 KiB
TypeScript
228 lines
8.2 KiB
TypeScript
import mongoose from 'mongoose';
|
|
|
|
const RecipeSchema = new mongoose.Schema(
|
|
{
|
|
short_name: {type: String, required: true, unique: true},
|
|
name : {type: String, required: true,},
|
|
category : {type: String, required: true,},
|
|
icon: {type: String, required: true},
|
|
dateCreated: {type: Date, default: Date.now},
|
|
dateModified: {type: Date, default: Date.now},
|
|
images: [ {
|
|
mediapath: {type: String, required: true}, // filename with hash for cache busting: e.g., "maccaroni.a1b2c3d4.webp"
|
|
alt: String,
|
|
caption: String,
|
|
color: String, // dominant color hex e.g. "#a1b2c3", used as loading placeholder
|
|
}],
|
|
description: {type: String, required: true},
|
|
note: {type: String},
|
|
tags : [String],
|
|
seasonRanges: [{
|
|
_id: false,
|
|
start: {
|
|
_id: false,
|
|
kind: { type: String, enum: ['fixed', 'liturgical'], required: true },
|
|
m: { type: Number },
|
|
d: { type: Number },
|
|
anchor: { type: String, enum: ['easter', 'ash-wednesday', 'palm-sunday', 'pentecost', 'advent-i'] },
|
|
offsetDays: { type: Number, default: 0 },
|
|
},
|
|
end: {
|
|
_id: false,
|
|
kind: { type: String, enum: ['fixed', 'liturgical'], required: true },
|
|
m: { type: Number },
|
|
d: { type: Number },
|
|
anchor: { type: String, enum: ['easter', 'ash-wednesday', 'palm-sunday', 'pentecost', 'advent-i'] },
|
|
offsetDays: { type: Number, default: 0 },
|
|
},
|
|
}],
|
|
baking: { temperature: {type:String, default: ""},
|
|
length: {type:String, default: ""},
|
|
mode: {type:String, default: ""},
|
|
},
|
|
preparation : {type:String, default: ""},
|
|
fermentation: { bulk: {type:String, default: ""},
|
|
final: {type:String, default: ""},
|
|
|
|
},
|
|
portions :{type:String, default: ""},
|
|
defaultForm: {
|
|
shape: { type: String, enum: ['round', 'rectangular', 'gugelhupf'] },
|
|
diameter: { type: Number },
|
|
width: { type: Number },
|
|
length: { type: Number },
|
|
innerDiameter: { type: Number },
|
|
},
|
|
cooking: {type:String, default: ""},
|
|
total_time : {type:String, default: ""},
|
|
ingredients: [{
|
|
// Common fields
|
|
name: { type: String, default: "" },
|
|
type: { type: String, enum: ['section', 'reference'], default: 'section' },
|
|
|
|
// For type='section' (existing structure)
|
|
list: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
|
|
// For type='reference' (new base recipe references)
|
|
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
|
|
includeIngredients: { type: Boolean, default: true },
|
|
showLabel: { type: Boolean, default: true },
|
|
labelOverride: { type: String, default: "" },
|
|
baseMultiplier: { type: Number, default: 1 },
|
|
itemsBefore: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
itemsAfter: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
}],
|
|
instructions: [{
|
|
// Common fields
|
|
name: { type: String, default: "" },
|
|
type: { type: String, enum: ['section', 'reference'], default: 'section' },
|
|
|
|
// For type='section' (existing structure)
|
|
steps: [String],
|
|
|
|
// For type='reference' (new base recipe references)
|
|
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
|
|
includeInstructions: { type: Boolean, default: true },
|
|
showLabel: { type: Boolean, default: true },
|
|
labelOverride: { type: String, default: "" },
|
|
baseMultiplier: { type: Number, default: 1 },
|
|
stepsBefore: [String],
|
|
stepsAfter: [String],
|
|
}],
|
|
preamble : String,
|
|
addendum : String,
|
|
|
|
// Base recipe flag
|
|
isBaseRecipe: {type: Boolean, default: false},
|
|
|
|
// English translations
|
|
translations: {
|
|
en: {
|
|
short_name: {type: String}, // English slug for URLs
|
|
name: {type: String},
|
|
description: {type: String},
|
|
preamble: {type: String},
|
|
addendum: {type: String},
|
|
note: {type: String},
|
|
category: {type: String},
|
|
tags: [String],
|
|
portions: {type: String},
|
|
preparation: {type: String},
|
|
cooking: {type: String},
|
|
total_time: {type: String},
|
|
baking: {
|
|
temperature: {type: String},
|
|
length: {type: String},
|
|
mode: {type: String},
|
|
},
|
|
fermentation: {
|
|
bulk: {type: String},
|
|
final: {type: String},
|
|
},
|
|
ingredients: [{
|
|
name: { type: String, default: "" },
|
|
type: { type: String, enum: ['section', 'reference'], default: 'section' },
|
|
list: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
|
|
includeIngredients: { type: Boolean, default: true },
|
|
showLabel: { type: Boolean, default: true },
|
|
labelOverride: { type: String, default: "" },
|
|
baseMultiplier: { type: Number, default: 1 },
|
|
itemsBefore: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
itemsAfter: [{
|
|
name: { type: String, default: "" },
|
|
unit: String,
|
|
amount: String,
|
|
}],
|
|
}],
|
|
instructions: [{
|
|
name: { type: String, default: "" },
|
|
type: { type: String, enum: ['section', 'reference'], default: 'section' },
|
|
steps: [String],
|
|
baseRecipeRef: { type: mongoose.Schema.Types.ObjectId, ref: 'Recipe' },
|
|
includeInstructions: { type: Boolean, default: true },
|
|
showLabel: { type: Boolean, default: true },
|
|
labelOverride: { type: String, default: "" },
|
|
baseMultiplier: { type: Number, default: 1 },
|
|
stepsBefore: [String],
|
|
stepsAfter: [String],
|
|
}],
|
|
images: [{
|
|
alt: String,
|
|
caption: String,
|
|
}],
|
|
translationStatus: {
|
|
type: String,
|
|
enum: ['pending', 'approved', 'needs_update'],
|
|
default: 'pending'
|
|
},
|
|
lastTranslated: {type: Date},
|
|
changedFields: [String],
|
|
}
|
|
},
|
|
|
|
// Nutrition calorie/macro mapping for each ingredient
|
|
nutritionMappings: [{
|
|
sectionIndex: { type: Number, required: true },
|
|
ingredientIndex: { type: Number, required: true },
|
|
ingredientName: { type: String },
|
|
ingredientNameDe: { type: String },
|
|
source: { type: String, enum: ['bls', 'usda', 'manual'] },
|
|
fdcId: { type: Number },
|
|
blsCode: { type: String },
|
|
nutritionDbName: { type: String },
|
|
matchConfidence: { type: Number },
|
|
matchMethod: { type: String, enum: ['exact', 'embedding', 'manual', 'none'] },
|
|
gramsPerUnit: { type: Number },
|
|
defaultAmountUsed: { type: Boolean, default: false },
|
|
unitConversionSource: { type: String, enum: ['direct', 'density', 'usda_portion', 'estimate', 'manual', 'none'] },
|
|
manuallyEdited: { type: Boolean, default: false },
|
|
excluded: { type: Boolean, default: false },
|
|
recipeRef: { type: String },
|
|
recipeRefMultiplier: { type: Number, default: 1 },
|
|
}],
|
|
|
|
// Cached nutrition per 100g (for round-off suggestions & listing)
|
|
cachedPer100g: { type: mongoose.Schema.Types.Mixed },
|
|
cachedTotalGrams: { type: Number },
|
|
|
|
// Translation metadata for tracking changes
|
|
translationMetadata: {
|
|
lastModifiedGerman: {type: Date},
|
|
fieldsModifiedSinceTranslation: [String],
|
|
},
|
|
}, {timestamps: true}
|
|
);
|
|
|
|
// Indexes for efficient querying
|
|
RecipeSchema.index({ short_name: 1 });
|
|
RecipeSchema.index({ 'seasonRanges.start.anchor': 1 });
|
|
RecipeSchema.index({ "translations.en.short_name": 1 });
|
|
RecipeSchema.index({ "translations.en.translationStatus": 1 });
|
|
|
|
import type { RecipeModelType } from '$types/types';
|
|
|
|
// Delete cached model on HMR so schema changes (e.g. new fields) are picked up
|
|
delete mongoose.models.Recipe;
|
|
export const Recipe = mongoose.model<RecipeModelType>("Recipe", RecipeSchema);
|