fix: remove all hardcoded workspaceId=65 + add robustness for large UATs
- enrich-service: resolve workspacePk from feature attrs / GisUat DB / ArcGIS (was hardcoded 65, broke enrichment for non-BN counties) - enrich-service: skip already-enriched features (resume after crash) - no-geom-sync: use resolved wsPk in synthetic attributes - no-geom-sync: batched DB inserts (50/batch) with retry + exponential backoff - Fixes: Magic export for Cluj/other counties getting empty enrichment
This commit is contained in:
@@ -170,6 +170,7 @@ export async function enrichFeatures(
|
||||
objectId: true,
|
||||
attributes: true,
|
||||
cadastralRef: true,
|
||||
enrichedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -187,6 +188,56 @@ export async function enrichFeatures(
|
||||
};
|
||||
}
|
||||
|
||||
// Resolve workspace PK from feature attributes or GisUat DB
|
||||
let resolvedWsPk: number | null = null;
|
||||
for (const f of terenuri) {
|
||||
const ws = (f.attributes as Record<string, unknown>).WORKSPACE_ID;
|
||||
if (ws != null) {
|
||||
const n = Number(ws);
|
||||
if (Number.isFinite(n) && n > 0) {
|
||||
resolvedWsPk = n;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!resolvedWsPk) {
|
||||
try {
|
||||
const row = await prisma.gisUat.findUnique({
|
||||
where: { siruta },
|
||||
select: { workspacePk: true },
|
||||
});
|
||||
if (row?.workspacePk && row.workspacePk > 0)
|
||||
resolvedWsPk = row.workspacePk;
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
if (!resolvedWsPk) {
|
||||
// Last resort: try ArcGIS layer query for 1 feature
|
||||
try {
|
||||
const features = await client.listLayer(
|
||||
{
|
||||
id: "TERENURI_ACTIVE",
|
||||
name: "TERENURI_ACTIVE",
|
||||
endpoint: "aut",
|
||||
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
|
||||
},
|
||||
siruta,
|
||||
{ limit: 1, outFields: "WORKSPACE_ID" },
|
||||
);
|
||||
const wsId = features?.[0]?.attributes?.WORKSPACE_ID;
|
||||
if (wsId != null) {
|
||||
const n = Number(wsId);
|
||||
if (Number.isFinite(n) && n > 0) resolvedWsPk = n;
|
||||
}
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
// If still null, enrichment will fail gracefully with empty lists
|
||||
const workspacePkForApi = resolvedWsPk ?? 65;
|
||||
console.log(`[enrich] siruta=${siruta} workspacePk=${workspacePkForApi}`);
|
||||
|
||||
push({
|
||||
phase: "Pregătire îmbogățire",
|
||||
downloaded: 0,
|
||||
@@ -261,7 +312,7 @@ export async function enrichFeatures(
|
||||
while (listPage < listTotalPages) {
|
||||
const listResponse = await throttled(() =>
|
||||
client.fetchImmovableListByAdminUnit(
|
||||
65,
|
||||
workspacePkForApi,
|
||||
siruta,
|
||||
listPage,
|
||||
200,
|
||||
@@ -299,7 +350,7 @@ export async function enrichFeatures(
|
||||
for (let i = 0; i < immovableIds.length; i += docBatchSize) {
|
||||
const batch = immovableIds.slice(i, i + docBatchSize);
|
||||
const docResponse = await throttled(() =>
|
||||
client.fetchDocumentationData(65, batch),
|
||||
client.fetchDocumentationData(workspacePkForApi, batch),
|
||||
);
|
||||
(docResponse?.immovables ?? []).forEach((item: any) => {
|
||||
const idKey = normalizeId(item?.immovablePk);
|
||||
@@ -348,6 +399,20 @@ export async function enrichFeatures(
|
||||
for (let index = 0; index < terenuri.length; index += 1) {
|
||||
const feature = terenuri[index]!;
|
||||
const attrs = feature.attributes as Record<string, unknown>;
|
||||
|
||||
// Skip features already enriched (resume after crash/interruption)
|
||||
if (feature.enrichedAt != null) {
|
||||
enrichedCount += 1;
|
||||
if (index % 50 === 0) {
|
||||
options?.onProgress?.(
|
||||
index + 1,
|
||||
terenuri.length,
|
||||
"Îmbogățire parcele (skip enriched)",
|
||||
);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const immovableId = attrs.IMMOVABLE_ID ?? "";
|
||||
const workspaceId = attrs.WORKSPACE_ID ?? "";
|
||||
const applicationId = (attrs.APPLICATION_ID as number) ?? null;
|
||||
|
||||
@@ -252,77 +252,109 @@ export async function syncNoGeometryParcels(
|
||||
return { imported: 0, skipped: 0, errors: 0, status: "done" };
|
||||
}
|
||||
|
||||
// 4. Import candidates
|
||||
// 4. Import candidates in batches with retry
|
||||
let imported = 0;
|
||||
let skipped = 0;
|
||||
let errors = 0;
|
||||
const total = candidates.length;
|
||||
const BATCH_SIZE = 50;
|
||||
const MAX_RETRIES = 3;
|
||||
|
||||
for (let i = 0; i < candidates.length; i++) {
|
||||
const item = candidates[i]!;
|
||||
const immPk = Number(item.immovablePk ?? 0);
|
||||
if (immPk <= 0) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
for (
|
||||
let batchStart = 0;
|
||||
batchStart < candidates.length;
|
||||
batchStart += BATCH_SIZE
|
||||
) {
|
||||
const batch = candidates.slice(batchStart, batchStart + BATCH_SIZE);
|
||||
const ops: Array<ReturnType<typeof prisma.gisFeature.upsert>> = [];
|
||||
|
||||
const cadRef = String(item.identifierDetails ?? "").trim();
|
||||
const areaValue = typeof item.area === "number" ? item.area : null;
|
||||
for (const item of batch) {
|
||||
const immPk = Number(item.immovablePk ?? 0);
|
||||
if (immPk <= 0) {
|
||||
skipped++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build synthetic attributes to match the eTerra GIS layer format
|
||||
const attributes: Record<string, unknown> = {
|
||||
OBJECTID: -immPk, // synthetic negative
|
||||
IMMOVABLE_ID: immPk,
|
||||
WORKSPACE_ID: item.workspacePk ?? 65,
|
||||
APPLICATION_ID: item.applicationId ?? null,
|
||||
NATIONAL_CADASTRAL_REFERENCE: cadRef,
|
||||
AREA_VALUE: areaValue,
|
||||
IS_ACTIVE: 1,
|
||||
ADMIN_UNIT_ID: Number(siruta),
|
||||
// Metadata from immovable list
|
||||
PAPER_CAD_NO: item.paperCadNo ?? null,
|
||||
PAPER_CF_NO: item.paperCfNo ?? null,
|
||||
PAPER_LB_NO: item.paperLbNo ?? null,
|
||||
TOP_NO: item.topNo ?? null,
|
||||
IMMOVABLE_TYPE: item.immovableType ?? "P",
|
||||
NO_GEOMETRY_SOURCE: "ETERRA_IMMOVABLE_LIST",
|
||||
};
|
||||
const cadRef = String(item.identifierDetails ?? "").trim();
|
||||
const areaValue = typeof item.area === "number" ? item.area : null;
|
||||
|
||||
try {
|
||||
await prisma.gisFeature.upsert({
|
||||
where: {
|
||||
layerId_objectId: {
|
||||
layerId: "TERENURI_ACTIVE",
|
||||
objectId: -immPk,
|
||||
const attributes: Record<string, unknown> = {
|
||||
OBJECTID: -immPk,
|
||||
IMMOVABLE_ID: immPk,
|
||||
WORKSPACE_ID: item.workspacePk ?? wsPk,
|
||||
APPLICATION_ID: item.applicationId ?? null,
|
||||
NATIONAL_CADASTRAL_REFERENCE: cadRef,
|
||||
AREA_VALUE: areaValue,
|
||||
IS_ACTIVE: 1,
|
||||
ADMIN_UNIT_ID: Number(siruta),
|
||||
PAPER_CAD_NO: item.paperCadNo ?? null,
|
||||
PAPER_CF_NO: item.paperCfNo ?? null,
|
||||
PAPER_LB_NO: item.paperLbNo ?? null,
|
||||
TOP_NO: item.topNo ?? null,
|
||||
IMMOVABLE_TYPE: item.immovableType ?? "P",
|
||||
NO_GEOMETRY_SOURCE: "ETERRA_IMMOVABLE_LIST",
|
||||
};
|
||||
|
||||
ops.push(
|
||||
prisma.gisFeature.upsert({
|
||||
where: {
|
||||
layerId_objectId: {
|
||||
layerId: "TERENURI_ACTIVE",
|
||||
objectId: -immPk,
|
||||
},
|
||||
},
|
||||
},
|
||||
create: {
|
||||
layerId: "TERENURI_ACTIVE",
|
||||
siruta,
|
||||
objectId: -immPk,
|
||||
cadastralRef: cadRef || null,
|
||||
areaValue,
|
||||
isActive: true,
|
||||
attributes: attributes as Prisma.InputJsonValue,
|
||||
geometry: Prisma.JsonNull,
|
||||
geometrySource: "NO_GEOMETRY",
|
||||
},
|
||||
update: {
|
||||
cadastralRef: cadRef || null,
|
||||
areaValue,
|
||||
attributes: attributes as Prisma.InputJsonValue,
|
||||
geometrySource: "NO_GEOMETRY",
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
});
|
||||
imported++;
|
||||
} catch {
|
||||
errors++;
|
||||
create: {
|
||||
layerId: "TERENURI_ACTIVE",
|
||||
siruta,
|
||||
objectId: -immPk,
|
||||
cadastralRef: cadRef || null,
|
||||
areaValue,
|
||||
isActive: true,
|
||||
attributes: attributes as Prisma.InputJsonValue,
|
||||
geometry: Prisma.JsonNull,
|
||||
geometrySource: "NO_GEOMETRY",
|
||||
},
|
||||
update: {
|
||||
cadastralRef: cadRef || null,
|
||||
areaValue,
|
||||
attributes: attributes as Prisma.InputJsonValue,
|
||||
geometrySource: "NO_GEOMETRY",
|
||||
updatedAt: new Date(),
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
if (i % 20 === 0 || i === total - 1) {
|
||||
options?.onProgress?.(i + 1, total, "Import parcele fără geometrie");
|
||||
// Execute batch with retry
|
||||
if (ops.length > 0) {
|
||||
let attempt = 0;
|
||||
while (attempt < MAX_RETRIES) {
|
||||
try {
|
||||
await prisma.$transaction(ops);
|
||||
imported += ops.length;
|
||||
break;
|
||||
} catch (err) {
|
||||
attempt++;
|
||||
if (attempt >= MAX_RETRIES) {
|
||||
// Fall back to individual upserts for this batch
|
||||
for (const op of ops) {
|
||||
try {
|
||||
await op;
|
||||
imported++;
|
||||
} catch {
|
||||
errors++;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Wait before retry (exponential backoff)
|
||||
await new Promise((r) => setTimeout(r, 500 * attempt));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const done = Math.min(batchStart + BATCH_SIZE, total);
|
||||
options?.onProgress?.(done, total, "Import parcele fără geometrie");
|
||||
}
|
||||
|
||||
return { imported, skipped, errors, status: "done" };
|
||||
|
||||
Reference in New Issue
Block a user