Files
ArchiTools/src/modules/parcel-sync/services/no-geom-sync.ts
T
AI Assistant dfb5ceb926 fix(parcel-sync): batch deleteMany to avoid PostgreSQL 32767 bind variable limit
Cluj-Napoca sync failed with 62,307 removed features exceeding the limit.
Batch deletions in chunks of 30,000 in both sync-service and no-geom-sync.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-28 10:17:57 +02:00

802 lines
25 KiB
TypeScript

/* eslint-disable @typescript-eslint/no-explicit-any */
/**
* No-geometry sync — imports eTerra immovables that have NO geometry
* in the GIS layer (TERENURI_ACTIVE).
*
* These are parcels that exist in the eTerra immovable database but
* have no spatial representation in the ArcGIS layer. They are stored
* in GisFeature with:
* - geometry = null
* - geometrySource = "NO_GEOMETRY"
* - objectId = negative immovablePk (to avoid collisions with real OBJECTIDs)
*
* The cross-reference works by:
* 1. Fetch full immovable list from eTerra for the UAT (paginated)
* 2. Load all existing cadastralRefs from DB for TERENURI_ACTIVE + siruta
* 3. Immovables whose cadastralRef is NOT in DB → candidates
* 4. Store each candidate as a GisFeature with geometry=null
*/
import { Prisma, PrismaClient } from "@prisma/client";
import { EterraClient } from "./eterra-client";
const prisma = new PrismaClient();
/* ------------------------------------------------------------------ */
/* Workspace resolution (county → eTerra workspace PK) */
/* ------------------------------------------------------------------ */
/**
* Resolve the eTerra workspace PK for a SIRUTA.
* Chain: explicit param → GisUat DB row → ArcGIS layer query → null.
*/
async function resolveWorkspacePk(
client: EterraClient,
siruta: string,
explicitPk?: number | null,
): Promise<number | null> {
// 1. Explicit param
if (explicitPk && Number.isFinite(explicitPk) && explicitPk > 0) {
return explicitPk;
}
// 2. DB lookup
try {
const row = await prisma.gisUat.findUnique({
where: { siruta },
select: { workspacePk: true },
});
if (row?.workspacePk && row.workspacePk > 0) return row.workspacePk;
} catch {
/* ignore */
}
// 3. ArcGIS layer query — fetch 1 feature from TERENURI_ACTIVE for this siruta
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 num = Number(wsId);
if (Number.isFinite(num) && num > 0) {
// Persist for future lookups
prisma.gisUat
.update({ where: { siruta }, data: { workspacePk: num } })
.catch(() => {});
return num;
}
}
} catch {
/* ignore */
}
return null;
}
const normalizeId = (value: unknown) => {
if (value === null || value === undefined) return "";
const text = String(value).trim();
if (!text) return "";
return text.replace(/\.0$/, "");
};
const normalizeCadRef = (value: unknown) =>
normalizeId(value).replace(/\s+/g, "").toUpperCase();
/** Quality breakdown of no-geometry immovables from scan */
export type NoGeomQuality = {
/** Have electronic cadRef (identifierDetails non-empty) */
withCadRef: number;
/** Have paper cadastral number */
withPaperCad: number;
/** Have paper LB / CF (carte funciară) number — field is paperLbNo in API */
withPaperLb: number;
/** Have hasLandbook=1 flag from eTerra */
withLandbook: number;
/** Have area > 0 (measuredArea or legalArea) */
withArea: number;
/** status=1 (active) in eTerra */
withActiveStatus: number;
/** "Useful" = active AND has identification or area */
useful: number;
/** Filtered out: inactive, or no identification AND no area */
empty: number;
};
export type NoGeomScanResult = {
totalImmovables: number;
/** Immovables that matched a remote GIS feature (cross-ref, may vary) */
withGeometry: number;
/** Total features in the remote ArcGIS TERENURI_ACTIVE layer (stable) */
remoteGisCount: number;
/** Total features in the remote ArcGIS CLADIRI_ACTIVE layer (stable) */
remoteCladiriCount: number;
noGeomCount: number;
/** Match quality: how many matched by cadastral ref vs immovable ID */
matchedByRef: number;
matchedById: number;
/** Quality breakdown of no-geometry items */
qualityBreakdown: NoGeomQuality;
/** Sample of immovable identifiers without geometry */
samples: Array<{
immovablePk: number;
identifierDetails: string;
paperCadNo?: string;
paperLbNo?: string;
status?: number;
hasLandbook?: number;
measuredArea?: number;
}>;
/** Total features already in local DB (geometry + no-geom) */
localDbTotal: number;
/** Geometry features already synced in local DB */
localDbWithGeom: number;
/** No-geometry features already imported in local DB */
localDbNoGeom: number;
/** How many are already enriched (magic) in local DB */
localDbEnriched: number;
/** How many enriched features have complete/current enrichment schema */
localDbEnrichedComplete: number;
/** Whether local sync is fresh (< 7 days) */
localSyncFresh: boolean;
/** Timestamp of the scan (for audit trail) */
scannedAt: string;
/** Error message if workspace couldn't be resolved */
error?: string;
};
export type NoGeomSyncResult = {
imported: number;
skipped: number;
cleaned: number;
errors: number;
status: "done" | "error";
error?: string;
};
/**
* Scan: count how many eTerra immovables for this UAT have no geometry
* in the remote GIS layer (TERENURI_ACTIVE).
*
* Cross-references the eTerra immovable list against the REMOTE ArcGIS
* layer (lightweight fetch, no geometry download). This works correctly
* regardless of whether the user has synced to local DB yet.
*
* This does NOT write anything — it's a read-only operation.
*/
export async function scanNoGeometryParcels(
client: EterraClient,
siruta: string,
options?: {
onProgress?: (page: number, totalPages: number) => void;
workspacePk?: number | null;
},
): Promise<NoGeomScanResult> {
// 0. Resolve workspace
const wsPk = await resolveWorkspacePk(client, siruta, options?.workspacePk);
if (!wsPk) {
return {
totalImmovables: 0,
withGeometry: 0,
remoteGisCount: 0,
remoteCladiriCount: 0,
noGeomCount: 0,
matchedByRef: 0,
matchedById: 0,
qualityBreakdown: {
withCadRef: 0,
withPaperCad: 0,
withPaperLb: 0,
withLandbook: 0,
withArea: 0,
withActiveStatus: 0,
useful: 0,
empty: 0,
},
samples: [],
localDbTotal: 0,
localDbWithGeom: 0,
localDbNoGeom: 0,
localDbEnriched: 0,
localDbEnrichedComplete: 0,
localSyncFresh: false,
scannedAt: new Date().toISOString(),
error: `Nu s-a putut determina workspace-ul (județul) pentru SIRUTA ${siruta}`,
};
}
// 1. Fetch all immovables from eTerra immovable list API
const allImmovables = await fetchAllImmovables(
client,
siruta,
wsPk,
options?.onProgress,
);
// 2. Fetch remote GIS cadastral refs (lightweight — no geometry)
// This is the source of truth for "has geometry" regardless of local DB state.
// Count first so pagination knows the total and doesn't stop early.
const terenuriLayer = {
id: "TERENURI_ACTIVE",
name: "TERENURI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
};
const [terenuriCount, cladiriCount] = await Promise.all([
client.countLayer(terenuriLayer, siruta).catch(() => 0),
client
.countLayer(
{
id: "CLADIRI_ACTIVE",
name: "CLADIRI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
},
siruta,
)
.catch(() => 0),
]);
const remoteFeatures = await client.fetchAllLayer(terenuriLayer, siruta, {
returnGeometry: false,
outFields: "OBJECTID,NATIONAL_CADASTRAL_REFERENCE,IMMOVABLE_ID",
pageSize: 1000,
total: terenuriCount > 0 ? terenuriCount : undefined,
});
// 2b. Also fetch CLADIRI_ACTIVE features (lightweight, just OBJECTID)
const cladiriLayer = {
id: "CLADIRI_ACTIVE",
name: "CLADIRI_ACTIVE",
endpoint: "aut" as const,
whereTemplate: "{{adminField}}={{siruta}} AND IS_ACTIVE=1",
};
let remoteCladiriCount = cladiriCount;
if (remoteCladiriCount === 0) {
try {
const cladiriFeatures = await client.fetchAllLayer(cladiriLayer, siruta, {
returnGeometry: false,
outFields: "OBJECTID",
pageSize: 1000,
});
remoteCladiriCount = cladiriFeatures.length;
} catch {
// Non-fatal — just won't show clădiri count
}
}
const remoteCadRefs = new Set<string>();
const remoteImmIds = new Set<string>();
for (const f of remoteFeatures) {
const cadRef = normalizeCadRef(
f.attributes?.NATIONAL_CADASTRAL_REFERENCE ?? "",
);
if (cadRef) remoteCadRefs.add(cadRef);
const immId = normalizeId(f.attributes?.IMMOVABLE_ID);
if (immId) remoteImmIds.add(immId);
}
// 3. Cross-reference: immovables NOT in remote GIS = no geometry
const noGeomItems: Array<{
immovablePk: number;
identifierDetails: string;
paperCadNo?: string;
paperLbNo?: string;
status?: number;
hasLandbook?: number;
measuredArea?: number;
legalArea?: number;
}> = [];
let matchedByRef = 0;
let matchedById = 0;
for (const item of allImmovables) {
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
const immPk = Number(item.immovablePk ?? 0);
const immId = normalizeId(item.immovablePk);
// Present in remote GIS layer by cadastral ref? → has geometry
if (cadRef && remoteCadRefs.has(cadRef)) {
matchedByRef++;
continue;
}
// Present in remote GIS layer by IMMOVABLE_ID? → has geometry
if (immId && remoteImmIds.has(immId)) {
matchedById++;
continue;
}
noGeomItems.push({
immovablePk: immPk,
identifierDetails: String(item.identifierDetails ?? ""),
paperCadNo: item.paperCadNo ?? undefined,
paperLbNo: item.paperLbNo ?? undefined,
status: typeof item.status === "number" ? item.status : undefined,
hasLandbook:
typeof item.hasLandbook === "number" ? item.hasLandbook : undefined,
measuredArea:
typeof item.measuredArea === "number" ? item.measuredArea : undefined,
legalArea:
typeof item.legalArea === "number" ? item.legalArea : undefined,
});
}
// 4. Query local DB for context (what's already synced/imported)
// Also check enrichment completeness — do enriched features have
// the current schema? (e.g., PROPRIETARI_VECHI added later)
const ENRICHMENT_REQUIRED_KEYS = [
"NR_CAD",
"NR_CF",
"PROPRIETARI",
"PROPRIETARI_VECHI",
"ADRESA",
"CATEGORIE_FOLOSINTA",
"HAS_BUILDING",
];
const [localTotal, localNoGeom, enrichedFeatures, lastSyncRun] =
await Promise.all([
prisma.gisFeature.count({
where: { layerId: "TERENURI_ACTIVE", siruta },
}),
prisma.gisFeature.count({
where: {
layerId: "TERENURI_ACTIVE",
siruta,
geometrySource: "NO_GEOMETRY",
},
}),
prisma.gisFeature.findMany({
where: {
layerId: "TERENURI_ACTIVE",
siruta,
enrichedAt: { not: null },
},
select: { enrichment: true },
}),
prisma.gisSyncRun.findFirst({
where: { siruta, layerId: "TERENURI_ACTIVE", status: "done" },
orderBy: { completedAt: "desc" },
select: { completedAt: true },
}),
]);
const localEnriched = enrichedFeatures.length;
let localEnrichedComplete = 0;
for (const f of enrichedFeatures) {
const e = f.enrichment as Record<string, unknown> | null;
if (
e &&
ENRICHMENT_REQUIRED_KEYS.every((k) => k in e && e[k] !== undefined)
) {
localEnrichedComplete++;
}
}
const localWithGeom = localTotal - localNoGeom;
const syncFresh = lastSyncRun?.completedAt
? Date.now() - lastSyncRun.completedAt.getTime() < 168 * 60 * 60 * 1000
: false;
// withGeometry = immovables that MATCHED a GIS feature (always adds up)
const matchedCount = allImmovables.length - noGeomItems.length;
console.log(
`[no-geom-scan] Match quality: ${matchedCount} total (${matchedByRef} by cadRef, ${matchedById} by immId)` +
` | GIS layer: ${remoteFeatures.length} features | Immovables: ${allImmovables.length}` +
` | Unmatched GIS: ${remoteFeatures.length - matchedCount}`,
);
// Quality analysis of no-geom items
let qWithCadRef = 0;
let qWithPaperCad = 0;
let qWithPaperLb = 0;
let qWithLandbook = 0;
let qWithArea = 0;
let qWithActiveStatus = 0;
let qUseful = 0;
let qEmpty = 0;
for (const item of noGeomItems) {
const hasCad = !!item.identifierDetails?.trim();
const hasPaperCad = !!item.paperCadNo?.trim();
const hasPaperLb = !!item.paperLbNo?.trim();
const hasArea =
(item.measuredArea != null && item.measuredArea > 0) ||
(item.legalArea != null && item.legalArea > 0);
const isActive = item.status === 1;
const hasLb = item.hasLandbook === 1;
if (hasCad) qWithCadRef++;
if (hasPaperCad) qWithPaperCad++;
if (hasPaperLb) qWithPaperLb++;
if (hasLb) qWithLandbook++;
if (hasArea) qWithArea++;
if (isActive) qWithActiveStatus++;
// "Useful" = ACTIVE + HAS_LANDBOOK (imobil electronic) + has identification OR area
// Matches the import quality gate — only IE items are worth importing
const hasIdentification = hasCad || hasPaperLb || hasPaperCad;
if (isActive && hasLb && (hasIdentification || hasArea)) qUseful++;
else qEmpty++;
}
return {
totalImmovables: allImmovables.length,
withGeometry: matchedCount,
remoteGisCount: remoteFeatures.length,
remoteCladiriCount,
noGeomCount: noGeomItems.length,
matchedByRef,
matchedById,
qualityBreakdown: {
withCadRef: qWithCadRef,
withPaperCad: qWithPaperCad,
withPaperLb: qWithPaperLb,
withLandbook: qWithLandbook,
withArea: qWithArea,
withActiveStatus: qWithActiveStatus,
useful: qUseful,
empty: qEmpty,
},
samples: noGeomItems.slice(0, 20),
localDbTotal: localTotal,
localDbWithGeom: localWithGeom,
localDbNoGeom: localNoGeom,
localDbEnriched: localEnriched,
localDbEnrichedComplete: localEnrichedComplete,
localSyncFresh: syncFresh,
scannedAt: new Date().toISOString(),
};
}
/**
* Import: store no-geometry immovables as GisFeature records.
*
* Uses negative immovablePk as objectId to avoid collision with
* real OBJECTID values from the GIS layer (always positive).
*/
export async function syncNoGeometryParcels(
client: EterraClient,
siruta: string,
options?: {
onProgress?: (done: number, total: number, phase: string) => void;
workspacePk?: number | null;
},
): Promise<NoGeomSyncResult> {
try {
// 0. Resolve workspace
const wsPk = await resolveWorkspacePk(client, siruta, options?.workspacePk);
if (!wsPk) {
return {
imported: 0,
skipped: 0,
cleaned: 0,
errors: 0,
status: "error",
error: `Nu s-a putut determina workspace-ul pentru SIRUTA ${siruta}`,
};
}
// 1. Fetch all immovables
options?.onProgress?.(0, 1, "Descărcare listă imobile (fără geometrie)");
const allImmovables = await fetchAllImmovables(client, siruta, wsPk);
// 2. Cleanup: remove stale/orphan no-geom records from DB
// Build a set of valid immovablePks from the fresh immovable list.
// Any NO_GEOMETRY record whose immovablePk is NOT in the fresh list
// (or whose immovable is inactive/empty) gets deleted.
const validImmPks = new Set<number>();
for (const item of allImmovables) {
const pk = Number(item.immovablePk ?? 0);
if (pk > 0) {
const status = typeof item.status === "number" ? item.status : 1;
const cadRef = (item.identifierDetails ?? "").toString().trim();
const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim();
const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();
const hasLandbook =
typeof item.hasLandbook === "number" ? item.hasLandbook : 0;
const hasArea =
(typeof item.measuredArea === "number" && item.measuredArea > 0) ||
(typeof item.legalArea === "number" && item.legalArea > 0);
const hasIdentification = !!cadRef || hasPaperLb || hasPaperCad;
// Only keep items that pass the quality gate (active + hasLandbook + identification/area)
if (
status === 1 &&
hasLandbook === 1 &&
(hasIdentification || hasArea)
) {
validImmPks.add(pk);
}
}
}
// Find stale no-geom records: objectId < 0 means no-geom (objectId = -immPk)
const existingNoGeom = await prisma.gisFeature.findMany({
where: {
layerId: "TERENURI_ACTIVE",
siruta,
geometrySource: "NO_GEOMETRY",
},
select: { id: true, objectId: true },
});
const staleIds: string[] = [];
for (const f of existingNoGeom) {
const immPk = -f.objectId; // objectId = -immovablePk for no-geom
if (!validImmPks.has(immPk)) {
staleIds.push(f.id);
}
}
if (staleIds.length > 0) {
const BATCH = 30_000;
for (let i = 0; i < staleIds.length; i += BATCH) {
await prisma.gisFeature.deleteMany({
where: { id: { in: staleIds.slice(i, i + BATCH) } },
});
}
console.log(
`[no-geom-sync] Cleanup: removed ${staleIds.length} stale/invalid no-geom records`,
);
}
// 3. Get existing features from DB (after cleanup)
const existingFeatures = await prisma.gisFeature.findMany({
where: { layerId: "TERENURI_ACTIVE", siruta },
select: { cadastralRef: true, objectId: true },
});
const existingCadRefs = new Set<string>();
const existingObjIds = new Set<number>();
for (const f of existingFeatures) {
if (f.cadastralRef) existingCadRefs.add(normalizeCadRef(f.cadastralRef));
existingObjIds.add(f.objectId);
}
// 4. Filter: not yet in DB + quality gate
// Quality: must be ACTIVE (status=1) AND hasLandbook=1 (IE) AND have identification OR area.
// Items without landbook are not electronic immovables — no CF data to extract.
let filteredOut = 0;
const candidates = allImmovables.filter((item) => {
const cadRef = normalizeCadRef(item.identifierDetails ?? "");
const immPk = Number(item.immovablePk ?? 0);
// Already in DB? → skip (not counted as filtered)
if (cadRef && existingCadRefs.has(cadRef)) return false;
if (immPk > 0 && existingObjIds.has(-immPk)) return false;
// Quality gate 1: must be active (status=1)
const status = typeof item.status === "number" ? item.status : 1;
if (status !== 1) {
filteredOut++;
return false;
}
// Quality gate 2: must be an electronic immovable (hasLandbook=1)
const hasLandbook =
typeof item.hasLandbook === "number" ? item.hasLandbook : 0;
if (hasLandbook !== 1) {
filteredOut++;
return false;
}
// Quality gate 3: must have identification OR area
const hasCadRef = !!cadRef;
const hasPaperLb = !!(item.paperLbNo ?? "").toString().trim();
const hasPaperCad = !!(item.paperCadNo ?? "").toString().trim();
const hasIdentification = hasCadRef || hasPaperLb || hasPaperCad;
// Area: measuredArea and legalArea are the only area fields on immovable/list
const hasArea =
(typeof item.measuredArea === "number" && item.measuredArea > 0) ||
(typeof item.legalArea === "number" && item.legalArea > 0);
if (!hasIdentification && !hasArea) {
filteredOut++;
return false;
}
return true;
});
if (candidates.length === 0) {
return {
imported: 0,
skipped: filteredOut,
cleaned: staleIds.length,
errors: 0,
status: "done",
};
}
// 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 batchStart = 0;
batchStart < candidates.length;
batchStart += BATCH_SIZE
) {
const batch = candidates.slice(batchStart, batchStart + BATCH_SIZE);
const ops: Array<ReturnType<typeof prisma.gisFeature.upsert>> = [];
for (const item of batch) {
const immPk = Number(item.immovablePk ?? 0);
if (immPk <= 0) {
skipped++;
continue;
}
const cadRef = String(item.identifierDetails ?? "").trim();
// Extract area — on immovable/list, the real fields are measuredArea and legalArea
const areaValue =
(typeof item.measuredArea === "number" && item.measuredArea > 0
? item.measuredArea
: null) ??
(typeof item.legalArea === "number" && item.legalArea > 0
? item.legalArea
: null);
const attributes: Record<string, unknown> = {
OBJECTID: -immPk,
IMMOVABLE_ID: immPk,
WORKSPACE_ID: item.workspace?.nomenPk ?? wsPk,
APPLICATION_ID: item.applicationId ?? null,
NATIONAL_CADASTRAL_REFERENCE: cadRef,
AREA_VALUE: areaValue,
IS_ACTIVE: item.status === 1 ? 1 : 0,
ADMIN_UNIT_ID: Number(siruta),
PAPER_CAD_NO: item.paperCadNo ?? null,
PAPER_LB_NO: item.paperLbNo ?? null,
HAS_LANDBOOK: item.hasLandbook ?? null,
TOP_NO: item.topNo ?? null,
IMMOVABLE_TYPE: item.immovableType ?? "P",
MEASURED_AREA: item.measuredArea ?? null,
LEGAL_AREA: item.legalArea ?? null,
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(),
},
}),
);
}
// 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: skipped + filteredOut,
cleaned: staleIds.length,
errors,
status: "done",
};
} catch (error) {
const msg = error instanceof Error ? error.message : "Unknown error";
return {
imported: 0,
skipped: 0,
cleaned: 0,
errors: 0,
status: "error",
error: msg,
};
}
}
/**
* Fetch all immovables from the eTerra immovable list for a UAT.
* Paginated — fetches all pages.
*/
async function fetchAllImmovables(
client: EterraClient,
siruta: string,
workspaceId: number,
onProgress?: (page: number, totalPages: number) => void,
): Promise<any[]> {
const all: any[] = [];
let page = 0;
let totalPages = 1;
let includeInscrisCF = true;
while (page < totalPages) {
const response = await client.fetchImmovableListByAdminUnit(
workspaceId,
siruta,
page,
200,
includeInscrisCF,
);
// Retry without CF filter if first page is empty
if (page === 0 && !(response?.content ?? []).length && includeInscrisCF) {
includeInscrisCF = false;
page = 0;
totalPages = 1;
continue;
}
totalPages =
typeof response?.totalPages === "number"
? response.totalPages
: totalPages;
const content = response?.content ?? [];
all.push(...content);
page++;
if (onProgress) {
onProgress(page, totalPages);
}
}
return all;
}