webhook returns 200 even when a trigger rule isn't satisfied, which would let the app's res.ok check falsely report success if the secret drifted. Set trigger-rule-mismatch-http-response-code: 403 so unauthorized triggers are clearly rejected. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
79 lines
2.4 KiB
TypeScript
79 lines
2.4 KiB
TypeScript
/**
|
|
* v0.10.0 Location-spine backfill (Release A).
|
|
*
|
|
* Moves the live Garden off the freeform `Plant.zone` string onto the shared
|
|
* `Location` tree WITHOUT data loss. Run once, right after `migrate deploy`:
|
|
*
|
|
* npx tsx prisma/scripts/backfill-locations.ts
|
|
*
|
|
* Idempotent: re-running only fills in plants that still lack a locationId and
|
|
* reuses Locations matched case-insensitively by name. Plant.zone is left
|
|
* intact (the dual-write/read safety net) — it is dropped in Release B.
|
|
*/
|
|
import { PrismaClient } from "@prisma/client";
|
|
import { createId } from "@paralleldrive/cuid2";
|
|
|
|
const db = new PrismaClient();
|
|
|
|
/** Normalize a zone string for dedup: trim + collapse inner whitespace, case-insensitive key. */
|
|
function normKey(zone: string): string {
|
|
return zone.trim().replace(/\s+/g, " ").toLowerCase();
|
|
}
|
|
|
|
async function main() {
|
|
// Plants that have a zone but no location yet.
|
|
const plants = await db.plant.findMany({
|
|
where: { zone: { not: null }, locationId: null },
|
|
select: { id: true, zone: true },
|
|
});
|
|
|
|
if (plants.length === 0) {
|
|
console.log("Backfill: no plants need a Location. Done.");
|
|
return;
|
|
}
|
|
|
|
// Seed the lookup with any AREA Locations that already exist (idempotent re-runs).
|
|
const existing = await db.location.findMany({
|
|
where: { kind: "AREA" },
|
|
select: { id: true, name: true },
|
|
});
|
|
const byKey = new Map<string, string>(); // normKey → locationId
|
|
for (const loc of existing) byKey.set(normKey(loc.name), loc.id);
|
|
|
|
let createdLocations = 0;
|
|
let mappedPlants = 0;
|
|
|
|
for (const plant of plants) {
|
|
const raw = (plant.zone ?? "").trim().replace(/\s+/g, " ");
|
|
if (!raw) continue; // empty/whitespace-only zone → leave unmapped
|
|
const key = normKey(raw);
|
|
|
|
let locationId = byKey.get(key);
|
|
if (!locationId) {
|
|
locationId = createId();
|
|
await db.location.create({
|
|
data: { id: locationId, name: raw, kind: "AREA" },
|
|
});
|
|
byKey.set(key, locationId);
|
|
createdLocations++;
|
|
}
|
|
|
|
await db.plant.update({ where: { id: plant.id }, data: { locationId } });
|
|
mappedPlants++;
|
|
}
|
|
|
|
console.log(
|
|
`Backfill complete: created ${createdLocations} Location(s), mapped ${mappedPlants} plant(s).`,
|
|
);
|
|
console.log(
|
|
"Review the Locations list in the UI and merge any near-duplicates before Release B drops Plant.zone.",
|
|
);
|
|
}
|
|
|
|
main()
|
|
.catch((e) => {
|
|
console.error("Backfill failed:", e);
|
|
process.exit(1);
|
|
})
|
|
.finally(() => db.$disconnect());
|