fix(parcel-sync): robust workspace resolution with direct nomen lookup
- Add fetchNomenByPk() to EterraClient single nomen entry lookup - resolveWorkspace() now tries fast path first: direct nomen lookup for SIRUTA walk parentNomenPk chain to find COUNTY (1-3 calls vs 42+) - Falls back to full county scan only if direct lookup fails - Search route: DB lookup as middle fallback between workspacePk and resolve - Debug logging to trace workspace resolution on production - Fix: try all possible UAT identifier fields (nomenPk, siruta, code, pk)
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
|
import { EterraClient } from "@/modules/parcel-sync/services/eterra-client";
|
||||||
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
|
import { getSessionCredentials } from "@/modules/parcel-sync/services/session-store";
|
||||||
|
import { prisma } from "@/core/storage/prisma";
|
||||||
|
|
||||||
export const runtime = "nodejs";
|
export const runtime = "nodejs";
|
||||||
export const dynamic = "force-dynamic";
|
export const dynamic = "force-dynamic";
|
||||||
@@ -30,6 +31,17 @@ globalRef.__eterraWorkspaceCache = workspaceCache;
|
|||||||
* until we find one whose nomenPk matches the SIRUTA.
|
* until we find one whose nomenPk matches the SIRUTA.
|
||||||
* Results are cached globally (survives hot-reload).
|
* Results are cached globally (survives hot-reload).
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* Resolve eTerra workspace nomenPk for a given SIRUTA.
|
||||||
|
*
|
||||||
|
* Strategy (fast path first):
|
||||||
|
* 1. Check in-memory cache
|
||||||
|
* 2. Direct nomen lookup: GET /api/adm/nomen/{siruta} → read parentNomenPk
|
||||||
|
* then GET parent to check if it's a COUNTY → that's the workspace
|
||||||
|
* 3. Full scan: fetch all counties → their UATs (slow fallback, ~42 calls)
|
||||||
|
*
|
||||||
|
* Results are cached globally.
|
||||||
|
*/
|
||||||
async function resolveWorkspace(
|
async function resolveWorkspace(
|
||||||
client: EterraClient,
|
client: EterraClient,
|
||||||
siruta: string,
|
siruta: string,
|
||||||
@@ -37,6 +49,40 @@ async function resolveWorkspace(
|
|||||||
const cached = workspaceCache.get(siruta);
|
const cached = workspaceCache.get(siruta);
|
||||||
if (cached !== undefined) return cached;
|
if (cached !== undefined) return cached;
|
||||||
|
|
||||||
|
// Fast path: direct nomen lookup
|
||||||
|
try {
|
||||||
|
const nomen = await client.fetchNomenByPk(siruta);
|
||||||
|
console.log("[resolveWorkspace] direct nomen lookup for", siruta, "→", JSON.stringify(nomen).slice(0, 500));
|
||||||
|
if (nomen) {
|
||||||
|
// Walk parent chain to find COUNTY
|
||||||
|
const parentPk = nomen?.parentNomenPk ?? nomen?.parentPk ?? nomen?.parent?.nomenPk;
|
||||||
|
if (parentPk) {
|
||||||
|
const parent = await client.fetchNomenByPk(parentPk);
|
||||||
|
console.log("[resolveWorkspace] parent nomen:", JSON.stringify(parent).slice(0, 500));
|
||||||
|
const parentType = String(parent?.nomenType ?? parent?.type ?? "").toUpperCase();
|
||||||
|
if (parentType === "COUNTY" || parentType === "JUDET") {
|
||||||
|
const countyPk = Number(parent?.nomenPk ?? parentPk);
|
||||||
|
workspaceCache.set(siruta, countyPk);
|
||||||
|
return countyPk;
|
||||||
|
}
|
||||||
|
// Maybe grandparent is the county (UAT → commune → county?)
|
||||||
|
const grandparentPk = parent?.parentNomenPk ?? parent?.parentPk;
|
||||||
|
if (grandparentPk) {
|
||||||
|
const grandparent = await client.fetchNomenByPk(grandparentPk);
|
||||||
|
const gpType = String(grandparent?.nomenType ?? grandparent?.type ?? "").toUpperCase();
|
||||||
|
if (gpType === "COUNTY" || gpType === "JUDET") {
|
||||||
|
const countyPk = Number(grandparent?.nomenPk ?? grandparentPk);
|
||||||
|
workspaceCache.set(siruta, countyPk);
|
||||||
|
return countyPk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Direct lookup failed — continue to full scan
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slow fallback: iterate all counties and their UATs
|
||||||
try {
|
try {
|
||||||
const counties = await client.fetchCounties();
|
const counties = await client.fetchCounties();
|
||||||
for (const county of counties) {
|
for (const county of counties) {
|
||||||
@@ -45,12 +91,17 @@ async function resolveWorkspace(
|
|||||||
try {
|
try {
|
||||||
const uats = await client.fetchAdminUnitsByCounty(countyPk);
|
const uats = await client.fetchAdminUnitsByCounty(countyPk);
|
||||||
for (const uat of uats) {
|
for (const uat of uats) {
|
||||||
const uatPk = String(uat?.nomenPk ?? uat?.pk ?? "");
|
// Try all possible identifier fields
|
||||||
if (uatPk) {
|
const candidates = [
|
||||||
workspaceCache.set(uatPk, Number(countyPk));
|
uat?.nomenPk,
|
||||||
|
uat?.siruta,
|
||||||
|
uat?.code,
|
||||||
|
uat?.pk,
|
||||||
|
].filter(Boolean).map(String);
|
||||||
|
for (const c of candidates) {
|
||||||
|
workspaceCache.set(c, Number(countyPk));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Check if our SIRUTA is now resolved
|
|
||||||
const resolved = workspaceCache.get(siruta);
|
const resolved = workspaceCache.get(siruta);
|
||||||
if (resolved !== undefined) return resolved;
|
if (resolved !== undefined) return resolved;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -58,7 +109,7 @@ async function resolveWorkspace(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// fallback: can't fetch counties
|
// Can't fetch counties
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -187,10 +238,33 @@ export async function POST(req: Request) {
|
|||||||
|
|
||||||
const client = await EterraClient.create(username, password);
|
const client = await EterraClient.create(username, password);
|
||||||
|
|
||||||
// Use provided workspacePk — or fall back to resolution
|
// Use provided workspacePk — or look up from DB — or resolve from eTerra
|
||||||
let workspaceId = body.workspacePk ?? null;
|
let workspaceId = body.workspacePk ?? null;
|
||||||
|
console.log(
|
||||||
|
"[search] siruta:",
|
||||||
|
siruta,
|
||||||
|
"body.workspacePk:",
|
||||||
|
body.workspacePk,
|
||||||
|
"workspaceId:",
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
if (!workspaceId || !Number.isFinite(workspaceId)) {
|
if (!workspaceId || !Number.isFinite(workspaceId)) {
|
||||||
|
// Try DB lookup first (cheap)
|
||||||
|
try {
|
||||||
|
const dbUat = await prisma.gisUat.findUnique({
|
||||||
|
where: { siruta },
|
||||||
|
select: { workspacePk: true },
|
||||||
|
});
|
||||||
|
console.log("[search] DB lookup result:", dbUat);
|
||||||
|
if (dbUat?.workspacePk) workspaceId = dbUat.workspacePk;
|
||||||
|
} catch (e) {
|
||||||
|
console.log("[search] DB lookup error:", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!workspaceId || !Number.isFinite(workspaceId)) {
|
||||||
|
console.log("[search] falling back to resolveWorkspace...");
|
||||||
workspaceId = await resolveWorkspace(client, siruta);
|
workspaceId = await resolveWorkspace(client, siruta);
|
||||||
|
console.log("[search] resolveWorkspace result:", workspaceId);
|
||||||
}
|
}
|
||||||
if (!workspaceId) {
|
if (!workspaceId) {
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
|
|||||||
@@ -328,21 +328,17 @@ export function ParcelSyncModule() {
|
|||||||
// Load UATs from local DB (fast — no eTerra needed)
|
// Load UATs from local DB (fast — no eTerra needed)
|
||||||
fetch("/api/eterra/uats")
|
fetch("/api/eterra/uats")
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(
|
.then((data: { uats?: UatEntry[] }) => {
|
||||||
(data: {
|
if (data.uats && data.uats.length > 0) {
|
||||||
uats?: UatEntry[];
|
setUatData(data.uats);
|
||||||
}) => {
|
} else {
|
||||||
if (data.uats && data.uats.length > 0) {
|
// DB empty — fall back to static uat.json (no county/workspace)
|
||||||
setUatData(data.uats);
|
fetch("/uat.json")
|
||||||
} else {
|
.then((res) => res.json())
|
||||||
// DB empty — fall back to static uat.json (no county/workspace)
|
.then((fallback: UatEntry[]) => setUatData(fallback))
|
||||||
fetch("/uat.json")
|
.catch(() => {});
|
||||||
.then((res) => res.json())
|
}
|
||||||
.then((fallback: UatEntry[]) => setUatData(fallback))
|
})
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
// API failed — fall back to static uat.json
|
// API failed — fall back to static uat.json
|
||||||
fetch("/uat.json")
|
fetch("/uat.json")
|
||||||
@@ -372,23 +368,19 @@ export function ParcelSyncModule() {
|
|||||||
// POST triggers sync check — only does full fetch if data changed
|
// POST triggers sync check — only does full fetch if data changed
|
||||||
fetch("/api/eterra/uats", { method: "POST" })
|
fetch("/api/eterra/uats", { method: "POST" })
|
||||||
.then((res) => res.json())
|
.then((res) => res.json())
|
||||||
.then(
|
.then((data: { synced?: boolean }) => {
|
||||||
(data: { synced?: boolean }) => {
|
if (data.synced) {
|
||||||
if (data.synced) {
|
// Data changed — reload from DB
|
||||||
// Data changed — reload from DB
|
fetch("/api/eterra/uats")
|
||||||
fetch("/api/eterra/uats")
|
.then((res) => res.json())
|
||||||
.then((res) => res.json())
|
.then((fresh: { uats?: UatEntry[] }) => {
|
||||||
.then(
|
if (fresh.uats && fresh.uats.length > 0) {
|
||||||
(fresh: { uats?: UatEntry[] }) => {
|
setUatData(fresh.uats);
|
||||||
if (fresh.uats && fresh.uats.length > 0) {
|
}
|
||||||
setUatData(fresh.uats);
|
})
|
||||||
}
|
.catch(() => {});
|
||||||
},
|
}
|
||||||
)
|
})
|
||||||
.catch(() => {});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.catch(() => {});
|
.catch(() => {});
|
||||||
}, [session.connected]);
|
}, [session.connected]);
|
||||||
|
|
||||||
|
|||||||
@@ -560,6 +560,20 @@ export class EterraClient {
|
|||||||
return this.getRawJson(url);
|
return this.getRawJson(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch a single nomenclature entry by nomenPk.
|
||||||
|
* Returns { nomenPk, name, parentNomenPk, nomenType, ... } or null
|
||||||
|
*/
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
async fetchNomenByPk(nomenPk: string | number): Promise<any> {
|
||||||
|
const url = `${BASE_URL}/api/adm/nomen/${nomenPk}`;
|
||||||
|
try {
|
||||||
|
return await this.getRawJson(url);
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch administrative units (UATs) under a county workspace.
|
* Fetch administrative units (UATs) under a county workspace.
|
||||||
* Returns array of { nomenPk, name, parentNomenPk, ... }
|
* Returns array of { nomenPk, name, parentNomenPk, ... }
|
||||||
|
|||||||
Reference in New Issue
Block a user