WORKFLOW-APP/app/api/pb/records/[id]/route.ts
Ysterbaard 42b4b2812b Fix structured-address + onboarding fields not persisting from edit modal
The /api/pb/records/[id] proxy had an allowlist of editable fields that
was written before the 2026-08-18 onboarding field additions. The new
fields were silently dropped before reaching PocketBase, so the modal
appeared to save successfully but the data never landed in PB.

Added to COMPANY_EDIT_FIELDS:
  physical_address_line_1/2, physical_town_city, physical_province,
  physical_postal_code, postal_address_line_1/2, postal_town_city,
  postal_province, postal_postal_code, company_email, company_cell_phone,
  public_officer_name, company_secretary_name, financial_year_end_month,
  authorised_shares

Added to DIRECTOR_EDIT_FIELDS:
  residential_address_line_1/2, residential_town_city, residential_province,
  residential_postal_code, postal_address_line_1/2, postal_town_city,
  postal_province, postal_postal_code, date_of_birth

End-to-end tested via proxy PATCH — fields now land in PB.
2026-08-19 10:20:11 +02:00

156 lines
5.2 KiB
TypeScript

// app/api/pb/records/[id]/route.ts
// GET /api/pb/records/:id?collection=... — fetch a single record
// PATCH /api/pb/records/:id?collection=... — update a record (edit)
// DELETE /api/pb/records/:id?collection=... — delete a record
import { NextRequest, NextResponse } from "next/server";
import { fetchRecord, updateRecord, deleteRecord } from "@/lib/pb";
export const dynamic = "force-dynamic";
const ALLOWED_COLLECTIONS = new Set([
"ysterbaard_directors",
"ysterbaard_companies",
"groups",
]);
// Fields editable for directors via the in-app edit form.
// (Address fields added 2026-08-18 alongside the onboarding form fields.)
const DIRECTOR_EDIT_FIELDS = new Set([
"surname", "first_names", "id_no", "id_no_masked",
"telephone", "email", "notes", "source",
"residential_address",
"residential_address_line_1", "residential_address_line_2",
"residential_town_city", "residential_province", "residential_postal_code",
"postal_address",
"postal_address_line_1", "postal_address_line_2",
"postal_town_city", "postal_province", "postal_postal_code",
"director_type",
"date_of_birth",
]);
// Fields editable for companies via the in-app edit form.
// (Structured address fields + company-level fields added 2026-08-18.)
const COMPANY_EDIT_FIELDS = new Set([
"enterprise_no", "enterprise_name", "enterprise_type",
"registration_date",
"physical_address",
"physical_address_line_1", "physical_address_line_2",
"physical_town_city", "physical_province", "physical_postal_code",
"postal_address",
"postal_address_line_1", "postal_address_line_2",
"postal_town_city", "postal_province", "postal_postal_code",
"company_email", "company_cell_phone",
"public_officer_name", "company_secretary_name",
"financial_year_end_month", "authorised_shares",
"director", "notes", "source",
"sars_login_username", "sars_login_password",
"sars_paye_no", "sars_vat_no", "sars_tax_number",
"sars_vat_returns_period", "sars_paye_return_period",
"groups",
]);
function getIdParam(params: { id: string }) {
return params.id;
}
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const url = new URL(req.url);
const collection = url.searchParams.get("collection");
const expand = url.searchParams.get("expand") ?? undefined;
if (!collection || !ALLOWED_COLLECTIONS.has(collection)) {
return NextResponse.json(
{ error: `collection '${collection}' is not allowed` },
{ status: 400 }
);
}
try {
const result = await fetchRecord(collection, getIdParam(params), expand);
return NextResponse.json(result);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown PB error";
console.error("[pb get]", collection, getIdParam(params), message);
return NextResponse.json({ error: message }, { status: 502 });
}
}
export async function PATCH(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const url = new URL(req.url);
const collection = url.searchParams.get("collection");
if (!collection || !ALLOWED_COLLECTIONS.has(collection)) {
return NextResponse.json(
{ error: `collection '${collection}' is not allowed` },
{ status: 400 }
);
}
let body: Record<string, unknown>;
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
// Sanitise: only pass through allowlisted fields.
const allowlist = collection === "ysterbaard_directors"
? DIRECTOR_EDIT_FIELDS
: COMPANY_EDIT_FIELDS;
const sanitised: Record<string, unknown> = {};
for (const key of allowlist) {
if (key in body) {
sanitised[key] = body[key];
}
}
// Recompute id_no_masked if id_no is being changed.
if ("id_no" in sanitised && typeof sanitised.id_no === "string" && sanitised.id_no.length >= 12) {
const id = sanitised.id_no as string;
sanitised.id_no_masked = id.slice(0, 6) + "XXXX" + id.slice(10, 12) + "X";
}
try {
const record = await updateRecord(collection, getIdParam(params), sanitised);
console.log(`[pb patch] ${new Date().toISOString()} ${collection}/${record.id}`);
return NextResponse.json(record);
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown PB error";
console.error("[pb patch]", collection, getIdParam(params), message);
return NextResponse.json({ error: message }, { status: 502 });
}
}
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const url = new URL(req.url);
const collection = url.searchParams.get("collection");
if (!collection || !ALLOWED_COLLECTIONS.has(collection)) {
return NextResponse.json(
{ error: `collection '${collection}' is not allowed` },
{ status: 400 }
);
}
const id = getIdParam(params);
try {
await deleteRecord(collection, id);
console.log(`[pb delete] ${new Date().toISOString()} ${collection}/${id}`);
return new NextResponse(null, { status: 204 });
} catch (err) {
const message = err instanceof Error ? err.message : "Unknown PB error";
console.error("[pb delete]", collection, id, message);
return NextResponse.json({ error: message }, { status: 502 });
}
}