Files
Moonbase/src/app/api/locations/route.ts
tonym 9c0312676a v0.10.0 — Location spine: garden moved onto a shared place tree
Completes the Location feature (schema + migration landed earlier under 98cabc5).

- Hierarchical Location model is the shared spine; unlimited nesting
- Garden add/edit now picks a reusable Location (inline create) instead of
  freeform text; garden groups By Location off the real tree
- Dual-write keeps legacy Plant.zone in sync with location.name (Release A)
- /api/locations CRUD + /api/locations/[id]/log (logs a location + its sub-locations)
- Location tree pure-core (buildTree/computePath/descendantIds) with tests
- Location added to backup/restore export
- Bump APP_VERSION 0.10.0 + plain-English changelog entry

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 23:36:54 -05:00

79 lines
2.7 KiB
TypeScript

import { NextResponse } from "next/server";
import { z } from "zod";
import { LocationKind } from "@prisma/client";
import { db } from "@/lib/db";
import { requireAuth } from "@/lib/auth";
import { recomputeAllPaths } from "@/lib/locations/db";
const createSchema = z.object({
name: z.string().min(1),
kind: z.nativeEnum(LocationKind).optional(),
parentId: z.string().optional(),
notes: z.string().optional(),
});
// GET /api/locations — flat list of active locations (client builds the tree).
export async function GET() {
try {
await requireAuth();
const locations = await db.location.findMany({
where: { active: true },
orderBy: { name: "asc" },
include: { _count: { select: { plants: { where: { active: true } } } } },
});
return NextResponse.json(locations);
} catch (err) {
if (err instanceof Error && err.message === "Unauthorized")
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
console.error(err);
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}
// POST /api/locations — create a location node.
export async function POST(req: Request) {
try {
const session = await requireAuth();
const data = createSchema.parse(await req.json());
const name = data.name.trim();
if (!name) return NextResponse.json({ error: "Name is required" }, { status: 400 });
// Validate parent exists (if given).
if (data.parentId) {
const parent = await db.location.findUnique({ where: { id: data.parentId } });
if (!parent) return NextResponse.json({ error: "Parent location not found" }, { status: 400 });
}
const location = await db.$transaction(async (tx) => {
const created = await tx.location.create({
data: {
name,
kind: data.kind ?? LocationKind.AREA,
parentId: data.parentId || null,
notes: data.notes?.trim() || null,
},
});
await recomputeAllPaths(tx as unknown as typeof db);
await tx.auditEvent.create({
data: {
action: "location.create",
entityId: created.id,
actorId: session.user.id,
payload: { name: created.name, kind: created.kind, parentId: created.parentId },
},
});
return created;
});
return NextResponse.json(location, { status: 201 });
} catch (err) {
if (err instanceof z.ZodError)
return NextResponse.json({ error: err.issues[0]?.message ?? "Invalid data" }, { status: 400 });
if (err instanceof Error && err.message === "Unauthorized")
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
console.error(err);
return NextResponse.json({ error: "Server error" }, { status: 500 });
}
}