208 lines
5.7 KiB
TypeScript
208 lines
5.7 KiB
TypeScript
import { PrismaClient, type HolidayCalendarEntry } from "@prisma/client";
|
|
import { buildHolidayCalendarSeedDefinitions } from "./holiday-calendar-seed-data.js";
|
|
import { loadWorkspaceEnv } from "./load-workspace-env.js";
|
|
import { assertCapaKrakenDbTarget } from "./safe-destructive-env.js";
|
|
|
|
loadWorkspaceEnv();
|
|
|
|
const prisma = new PrismaClient();
|
|
|
|
const YEARS = [2026, 2027];
|
|
const SEED_SOURCE = "seed:holiday-calendars:2026-2027";
|
|
|
|
type ExistingCalendar = {
|
|
id: string;
|
|
entries: Pick<HolidayCalendarEntry, "id" | "date" | "source">[];
|
|
};
|
|
|
|
function toUtcDate(isoDate: string): Date {
|
|
return new Date(`${isoDate}T00:00:00.000Z`);
|
|
}
|
|
|
|
function dateKey(value: Date): string {
|
|
return value.toISOString().slice(0, 10);
|
|
}
|
|
|
|
async function findScopedCalendar(input: {
|
|
countryId: string;
|
|
scopeType: "COUNTRY" | "STATE" | "CITY";
|
|
stateCode?: string | undefined;
|
|
metroCityId?: string | undefined;
|
|
}): Promise<ExistingCalendar | null> {
|
|
return prisma.holidayCalendar.findFirst({
|
|
where: {
|
|
countryId: input.countryId,
|
|
scopeType: input.scopeType,
|
|
stateCode: input.scopeType === "STATE" ? input.stateCode ?? null : null,
|
|
metroCityId: input.scopeType === "CITY" ? input.metroCityId ?? null : null,
|
|
},
|
|
select: {
|
|
id: true,
|
|
entries: {
|
|
select: {
|
|
id: true,
|
|
date: true,
|
|
source: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
|
|
async function main() {
|
|
assertCapaKrakenDbTarget("db:seed:holidays");
|
|
console.log("Seeding holiday calendars for 2026-2027...");
|
|
|
|
const countries = await prisma.country.findMany({
|
|
select: {
|
|
id: true,
|
|
code: true,
|
|
name: true,
|
|
metroCities: {
|
|
select: {
|
|
id: true,
|
|
name: true,
|
|
},
|
|
},
|
|
},
|
|
orderBy: { code: "asc" },
|
|
});
|
|
|
|
const activeGermanStatesRows = await prisma.resource.findMany({
|
|
where: {
|
|
isActive: true,
|
|
country: { code: "DE" },
|
|
federalState: { not: null },
|
|
},
|
|
select: { federalState: true },
|
|
distinct: ["federalState"],
|
|
});
|
|
|
|
const definitions = buildHolidayCalendarSeedDefinitions({
|
|
availableCountryCodes: countries.map((country) => country.code),
|
|
availableCitiesByCountry: Object.fromEntries(
|
|
countries.map((country) => [
|
|
country.code,
|
|
country.metroCities.map((city) => city.name),
|
|
]),
|
|
),
|
|
activeGermanStates: activeGermanStatesRows
|
|
.map((row) => row.federalState)
|
|
.filter((stateCode): stateCode is string => Boolean(stateCode)),
|
|
years: YEARS,
|
|
});
|
|
|
|
const countryByCode = new Map(countries.map((country) => [country.code, country]));
|
|
const cityByCountryAndName = new Map(
|
|
countries.flatMap((country) =>
|
|
country.metroCities.map((city) => [`${country.code}:${city.name}`, city] as const),
|
|
),
|
|
);
|
|
|
|
let createdCalendars = 0;
|
|
let reusedCalendars = 0;
|
|
let createdEntries = 0;
|
|
let updatedEntries = 0;
|
|
let skippedManualEntries = 0;
|
|
|
|
for (const definition of definitions) {
|
|
const country = countryByCode.get(definition.countryCode);
|
|
if (!country) {
|
|
continue;
|
|
}
|
|
|
|
const metroCity = definition.cityName
|
|
? cityByCountryAndName.get(`${definition.countryCode}:${definition.cityName}`)
|
|
: null;
|
|
|
|
if (definition.scopeType === "CITY" && !metroCity) {
|
|
console.warn(
|
|
`Skipping city calendar ${definition.name}: city ${definition.cityName ?? "?"} not found.`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
let calendar = await findScopedCalendar({
|
|
countryId: country.id,
|
|
scopeType: definition.scopeType,
|
|
stateCode: definition.stateCode,
|
|
metroCityId: metroCity?.id,
|
|
});
|
|
|
|
if (!calendar) {
|
|
calendar = await prisma.holidayCalendar.create({
|
|
data: {
|
|
name: definition.name,
|
|
scopeType: definition.scopeType,
|
|
countryId: country.id,
|
|
stateCode: definition.scopeType === "STATE" ? definition.stateCode ?? null : null,
|
|
metroCityId: definition.scopeType === "CITY" ? metroCity?.id ?? null : null,
|
|
priority: definition.priority,
|
|
isActive: true,
|
|
},
|
|
select: {
|
|
id: true,
|
|
entries: {
|
|
select: {
|
|
id: true,
|
|
date: true,
|
|
source: true,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
createdCalendars += 1;
|
|
} else {
|
|
reusedCalendars += 1;
|
|
}
|
|
|
|
const entriesByDate = new Map(calendar.entries.map((entry) => [dateKey(entry.date), entry]));
|
|
|
|
for (const entry of definition.entries) {
|
|
const existingEntry = entriesByDate.get(entry.date);
|
|
|
|
if (!existingEntry) {
|
|
await prisma.holidayCalendarEntry.create({
|
|
data: {
|
|
holidayCalendarId: calendar.id,
|
|
date: toUtcDate(entry.date),
|
|
name: entry.name,
|
|
isRecurringAnnual: entry.isRecurringAnnual,
|
|
source: SEED_SOURCE,
|
|
},
|
|
});
|
|
createdEntries += 1;
|
|
continue;
|
|
}
|
|
|
|
if (existingEntry.source && existingEntry.source !== SEED_SOURCE) {
|
|
skippedManualEntries += 1;
|
|
continue;
|
|
}
|
|
|
|
await prisma.holidayCalendarEntry.update({
|
|
where: { id: existingEntry.id },
|
|
data: {
|
|
name: entry.name,
|
|
isRecurringAnnual: entry.isRecurringAnnual,
|
|
source: SEED_SOURCE,
|
|
},
|
|
});
|
|
updatedEntries += 1;
|
|
}
|
|
}
|
|
|
|
console.log(` calendars created: ${createdCalendars}`);
|
|
console.log(` calendars reused: ${reusedCalendars}`);
|
|
console.log(` entries created: ${createdEntries}`);
|
|
console.log(` entries updated: ${updatedEntries}`);
|
|
console.log(` manual entries preserved: ${skippedManualEntries}`);
|
|
}
|
|
|
|
main()
|
|
.catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
})
|
|
.finally(() => prisma.$disconnect());
|