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.
108 lines
3.3 KiB
TypeScript
108 lines
3.3 KiB
TypeScript
/**
|
||
* One-time migration: convert legacy `season: number[]` (months 1–12) on every
|
||
* Recipe document to the new `seasonRanges: SeasonRange[]` shape.
|
||
*
|
||
* Contiguous months are coalesced into a single range. A wrap across the year
|
||
* boundary (e.g. months [11, 12, 1, 2]) merges into one wrapping range
|
||
* Nov 1 → Feb 28; non-contiguous months stay as separate ranges.
|
||
*
|
||
* The legacy `season` field is then $unset.
|
||
*
|
||
* Run before deploying the new code path:
|
||
* pnpm exec vite-node scripts/migrate-season-to-ranges.ts
|
||
*
|
||
* Idempotent: a recipe with no `season` field is left untouched.
|
||
*/
|
||
|
||
import { readFileSync } from 'fs';
|
||
import { resolve } from 'path';
|
||
import mongoose from 'mongoose';
|
||
|
||
const envPath = resolve(import.meta.dirname ?? '.', '..', '.env');
|
||
const envText = readFileSync(envPath, 'utf-8');
|
||
const mongoMatch = envText.match(/^MONGO_URL="?([^"\n]+)"?/m);
|
||
if (!mongoMatch) { console.error('MONGO_URL not found in .env'); process.exit(1); }
|
||
const MONGO_URL = mongoMatch[1];
|
||
|
||
const LAST_DAY = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
|
||
|
||
type FixedRange = { startM: number; endM: number };
|
||
|
||
/**
|
||
* Coalesce a set of months (1–12) into contiguous ranges, merging the
|
||
* year-boundary wrap if both Jan and Dec runs are present.
|
||
*/
|
||
function coalesceMonths(months: number[]): FixedRange[] {
|
||
const sorted = [...new Set(months.filter(m => Number.isInteger(m) && m >= 1 && m <= 12))].sort((a, b) => a - b);
|
||
if (sorted.length === 0) return [];
|
||
|
||
const runs: FixedRange[] = [];
|
||
let runStart = sorted[0];
|
||
let runEnd = sorted[0];
|
||
for (let i = 1; i < sorted.length; i++) {
|
||
if (sorted[i] === runEnd + 1) {
|
||
runEnd = sorted[i];
|
||
} else {
|
||
runs.push({ startM: runStart, endM: runEnd });
|
||
runStart = sorted[i];
|
||
runEnd = sorted[i];
|
||
}
|
||
}
|
||
runs.push({ startM: runStart, endM: runEnd });
|
||
|
||
// Merge the trailing-Dec run into the leading-Jan run so a winter span
|
||
// like [11,12,1,2] becomes one wrapping Nov→Feb range instead of two.
|
||
if (runs.length >= 2 && runs[0].startM === 1 && runs[runs.length - 1].endM === 12) {
|
||
const wrapped = { startM: runs[runs.length - 1].startM, endM: runs[0].endM };
|
||
return [wrapped, ...runs.slice(1, -1)];
|
||
}
|
||
return runs;
|
||
}
|
||
|
||
function rangeFromRun(run: FixedRange) {
|
||
return {
|
||
start: { kind: 'fixed', m: run.startM, d: 1 },
|
||
end: { kind: 'fixed', m: run.endM, d: LAST_DAY[run.endM - 1] }
|
||
};
|
||
}
|
||
|
||
async function main() {
|
||
await mongoose.connect(MONGO_URL);
|
||
const Recipe = mongoose.connection.collection('recipes');
|
||
|
||
const cursor = Recipe.find({ season: { $exists: true } });
|
||
let migrated = 0;
|
||
let skipped = 0;
|
||
|
||
while (await cursor.hasNext()) {
|
||
const doc = await cursor.next() as any;
|
||
if (!doc) break;
|
||
|
||
const months: number[] = Array.isArray(doc.season) ? doc.season : [];
|
||
const runs = coalesceMonths(months);
|
||
|
||
if (runs.length === 0) {
|
||
await Recipe.updateOne({ _id: doc._id }, { $unset: { season: '' } });
|
||
skipped++;
|
||
continue;
|
||
}
|
||
|
||
const seasonRanges = runs.map(rangeFromRun);
|
||
|
||
await Recipe.updateOne(
|
||
{ _id: doc._id },
|
||
{ $set: { seasonRanges }, $unset: { season: '' } }
|
||
);
|
||
migrated++;
|
||
if (migrated % 25 === 0) console.log(` migrated ${migrated}…`);
|
||
}
|
||
|
||
console.log(`\nDone. Migrated: ${migrated}. Skipped (empty season): ${skipped}.`);
|
||
await mongoose.disconnect();
|
||
}
|
||
|
||
main().catch((e) => {
|
||
console.error(e);
|
||
process.exit(1);
|
||
});
|