A rich-but-all-optional reference record per kind of plant (care, sun, water, bloom & harvest windows, toxic-to-pets, edible parts, spacing), distinct from a specific planting or a group. - PlantType model + optional Plant.plantTypeId; migration + idempotent backfill that seeds 247 kinds from the built-in plant library and links plantings by species/common name (wired into the deploy entrypoint) - /api/plant-types CRUD; new "Plant types" section (browse by category, edit a kind, see every spot you've planted it) - New plants auto-link to their type on create; plant detail shows "More about…" - PlantType added to backup/restore; bump APP_VERSION 0.11.0 + changelog Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
101 lines
4.5 KiB
TypeScript
101 lines
4.5 KiB
TypeScript
import { PrismaClient, PlantCategory } from "@prisma/client";
|
||
import { hashSync } from "bcryptjs";
|
||
import { syncPlantTypesFromLibrary } from "../src/lib/garden/plant-types";
|
||
|
||
const db = new PrismaClient();
|
||
|
||
async function main() {
|
||
console.log("Seeding Moon Base...");
|
||
|
||
// Admin accounts — only created if they don't exist yet.
|
||
const password = hashSync("moonbase!", 10);
|
||
|
||
await db.user.upsert({
|
||
where: { username: "bonna" },
|
||
update: { passwordHash: password },
|
||
create: {
|
||
id: "cluser0000000bonna",
|
||
email: "bonna@moonbase.local",
|
||
username: "bonna",
|
||
name: "Bonna Moon",
|
||
passwordHash: password,
|
||
role: "ADMIN",
|
||
mustChangePassword: true,
|
||
},
|
||
});
|
||
|
||
await db.user.upsert({
|
||
where: { username: "tony" },
|
||
update: { passwordHash: password },
|
||
create: {
|
||
id: "cluser0000000tony0",
|
||
email: "tony@moonbase.local",
|
||
username: "tony",
|
||
name: "Tony Moon",
|
||
passwordHash: password,
|
||
role: "ADMIN",
|
||
mustChangePassword: true,
|
||
},
|
||
});
|
||
|
||
if (process.env.SEED_MODE === "minimal") {
|
||
console.log("Minimal seed complete (admin accounts only).");
|
||
return;
|
||
}
|
||
|
||
// Sample plants for dev — a small starter food forest
|
||
const plants = [
|
||
{ commonName: "Apple", variety: "Honeycrisp", category: PlantCategory.CANOPY_TREE, zone: "Back yard, north fence", source: "Jung's Nursery", plantedAt: new Date("2024-04-15") },
|
||
{ commonName: "Apple", variety: "Liberty", category: PlantCategory.CANOPY_TREE, zone: "Back yard, north fence", source: "Jung's Nursery", plantedAt: new Date("2024-04-15") },
|
||
{ commonName: "Pear", variety: "Bartlett", category: PlantCategory.CANOPY_TREE, zone: "Back yard, east side", source: "Jung's Nursery", plantedAt: new Date("2024-04-20") },
|
||
{ commonName: "Elderberry", variety: "Bob Gordon", category: PlantCategory.UNDERSTORY_TREE, zone: "Back fence, south", source: "Raintree Nursery", plantedAt: new Date("2024-05-01") },
|
||
{ commonName: "Serviceberry", species: "Amelanchier canadensis", category: PlantCategory.UNDERSTORY_TREE, zone: "Front yard", source: "local nursery", plantedAt: new Date("2023-04-10") },
|
||
{ commonName: "Hazelnut", variety: "American", category: PlantCategory.LARGE_SHRUB, zone: "Back yard, west fence", source: "Oikos Tree Crops", plantedAt: new Date("2024-04-15") },
|
||
{ commonName: "Comfrey", species: "Symphytum officinale", category: PlantCategory.HERB, zone: "Under apple trees", source: "from seed", plantedAt: new Date("2024-05-15"), notes: "Chop-and-drop around the apples. Cut before it flowers to prevent spreading." },
|
||
{ commonName: "Raspberry", variety: "Heritage", category: PlantCategory.SHRUB, zone: "Back fence, east", source: "division", plantedAt: new Date("2023-05-01") },
|
||
{ commonName: "Strawberry", variety: "Ozark Beauty", category: PlantCategory.GROUNDCOVER, zone: "Under pear tree", source: "Jung's Nursery", plantedAt: new Date("2024-05-10") },
|
||
{ commonName: "Garlic", variety: "Music", category: PlantCategory.BULB, zone: "Raised bed #1", source: "Keene Organics", plantedAt: new Date("2024-10-10") },
|
||
];
|
||
|
||
// Starter Location tree — a root property node with the garden areas under it.
|
||
const root = await db.location.upsert({
|
||
where: { id: "seed-loc-root" },
|
||
update: {},
|
||
create: { id: "seed-loc-root", name: "Moon Homestead", kind: "OTHER", path: "Moon Homestead" },
|
||
});
|
||
|
||
const zoneNames = Array.from(new Set(plants.map((p) => p.zone).filter(Boolean) as string[]));
|
||
const zoneToLocId = new Map<string, string>();
|
||
for (const z of zoneNames) {
|
||
const id = `seed-loc-${z.toLowerCase().replace(/[^a-z0-9]+/g, "-")}`;
|
||
await db.location.upsert({
|
||
where: { id },
|
||
update: {},
|
||
create: { id, name: z, kind: "AREA", parentId: root.id, path: `Moon Homestead › ${z}` },
|
||
});
|
||
zoneToLocId.set(z, id);
|
||
}
|
||
|
||
for (const p of plants) {
|
||
await db.plant.upsert({
|
||
where: { id: `seed-${p.commonName.toLowerCase().replace(/\s/g, "-")}-${p.variety?.toLowerCase().replace(/\s/g, "-") ?? "0"}` },
|
||
update: {},
|
||
create: {
|
||
id: `seed-${p.commonName.toLowerCase().replace(/\s/g, "-")}-${p.variety?.toLowerCase().replace(/\s/g, "-") ?? "0"}`,
|
||
...p,
|
||
locationId: p.zone ? zoneToLocId.get(p.zone) : undefined,
|
||
},
|
||
});
|
||
}
|
||
|
||
// Seed the PlantType library and link the sample plants to their kind.
|
||
const pt = await syncPlantTypesFromLibrary(db);
|
||
console.log(`Seeded ${pt.types} plant types; linked ${pt.linked} plantings.`);
|
||
|
||
console.log("Demo seed complete.");
|
||
}
|
||
|
||
main()
|
||
.catch(console.error)
|
||
.finally(() => db.$disconnect());
|