feat(parcel-sync): native PostGIS geometry support for QGIS
- Remove postgresqlExtensions/postgis from Prisma schema (PostGIS not yet installed) - Add prisma/postgis-setup.sql: trigger auto-converts GeoJSON→native geometry, GiST spatial index, QGIS-friendly views (gis_terenuri, gis_cladiri, etc.) - Add POST /api/eterra/setup-postgis endpoint (idempotent, runs all SQL setup) - Add safety-net raw SQL in sync-service: backfills geom after upsert phase - Add QGIS/PostGIS setup card in layer catalog UI with connection info - Schema comment documents the trigger-managed 'geom' column approach
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
/**
|
||||
* POST /api/eterra/setup-postgis
|
||||
*
|
||||
* One-time (idempotent) setup: adds native PostGIS geometry column,
|
||||
* trigger for auto-conversion, spatial index, and QGIS-friendly views.
|
||||
*
|
||||
* Safe to call multiple times — all operations use IF NOT EXISTS / CREATE OR REPLACE.
|
||||
*/
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function POST() {
|
||||
try {
|
||||
// Execute the setup SQL statements individually
|
||||
// (Prisma $executeRawUnsafe doesn't support multi-statement SQL)
|
||||
|
||||
// 1. Ensure PostGIS extension
|
||||
await prisma.$executeRawUnsafe(`CREATE EXTENSION IF NOT EXISTS postgis`);
|
||||
|
||||
// 2. Add native geometry column if missing
|
||||
await prisma.$executeRawUnsafe(`
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name = 'GisFeature' AND column_name = 'geom'
|
||||
) THEN
|
||||
ALTER TABLE "GisFeature" ADD COLUMN geom geometry(Geometry, 3844);
|
||||
END IF;
|
||||
END $$
|
||||
`);
|
||||
|
||||
// 3. Trigger function
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE FUNCTION gis_feature_sync_geom()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF NEW.geometry IS NOT NULL THEN
|
||||
BEGIN
|
||||
NEW.geom := ST_SetSRID(ST_GeomFromGeoJSON(NEW.geometry::text), 3844);
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
NEW.geom := NULL;
|
||||
END;
|
||||
ELSE
|
||||
NEW.geom := NULL;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql
|
||||
`);
|
||||
|
||||
// 4. Trigger
|
||||
await prisma.$executeRawUnsafe(
|
||||
`DROP TRIGGER IF EXISTS trg_gis_feature_sync_geom ON "GisFeature"`,
|
||||
);
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE TRIGGER trg_gis_feature_sync_geom
|
||||
BEFORE INSERT OR UPDATE OF geometry ON "GisFeature"
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION gis_feature_sync_geom()
|
||||
`);
|
||||
|
||||
// 5. Backfill existing features
|
||||
const backfilled: { count: bigint }[] = await prisma.$queryRawUnsafe(`
|
||||
WITH updated AS (
|
||||
UPDATE "GisFeature"
|
||||
SET geom = ST_SetSRID(ST_GeomFromGeoJSON(geometry::text), 3844)
|
||||
WHERE geometry IS NOT NULL AND geom IS NULL
|
||||
RETURNING id
|
||||
)
|
||||
SELECT count(*) FROM updated
|
||||
`);
|
||||
const backfilledCount = Number(backfilled[0]?.count ?? 0);
|
||||
|
||||
// 6. Spatial index
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE INDEX IF NOT EXISTS gis_feature_geom_idx
|
||||
ON "GisFeature" USING GIST (geom)
|
||||
`);
|
||||
|
||||
// 7. QGIS-friendly views
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE VIEW gis_features AS
|
||||
SELECT
|
||||
id,
|
||||
"layerId" AS layer_id,
|
||||
siruta,
|
||||
"objectId" AS object_id,
|
||||
"inspireId" AS inspire_id,
|
||||
"cadastralRef" AS cadastral_ref,
|
||||
"areaValue" AS area_value,
|
||||
"isActive" AS is_active,
|
||||
attributes,
|
||||
"projectId" AS project_id,
|
||||
"createdAt" AS created_at,
|
||||
"updatedAt" AS updated_at,
|
||||
geom
|
||||
FROM "GisFeature"
|
||||
WHERE geom IS NOT NULL
|
||||
`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE VIEW gis_terenuri AS
|
||||
SELECT * FROM gis_features
|
||||
WHERE layer_id LIKE 'TERENURI%' OR layer_id LIKE 'CADGEN_LAND%'
|
||||
`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE VIEW gis_cladiri AS
|
||||
SELECT * FROM gis_features
|
||||
WHERE layer_id LIKE 'CLADIRI%' OR layer_id LIKE 'CADGEN_BUILDING%'
|
||||
`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE VIEW gis_documentatii AS
|
||||
SELECT * FROM gis_features
|
||||
WHERE layer_id LIKE 'EXPERTIZA%'
|
||||
OR layer_id LIKE 'ZONE_INTERES%'
|
||||
OR layer_id LIKE 'RECEPTII%'
|
||||
`);
|
||||
|
||||
await prisma.$executeRawUnsafe(`
|
||||
CREATE OR REPLACE VIEW gis_administrativ AS
|
||||
SELECT * FROM gis_features
|
||||
WHERE layer_id LIKE 'LIMITE%'
|
||||
OR layer_id LIKE 'SPECIAL_AREAS%'
|
||||
`);
|
||||
|
||||
// Count total features with native geometry
|
||||
const total: { count: bigint }[] = await prisma.$queryRawUnsafe(`
|
||||
SELECT count(*) FROM "GisFeature" WHERE geom IS NOT NULL
|
||||
`);
|
||||
const totalWithGeom = Number(total[0]?.count ?? 0);
|
||||
|
||||
return Response.json({
|
||||
success: true,
|
||||
message: "PostGIS setup complet",
|
||||
details: {
|
||||
backfilledFeatures: backfilledCount,
|
||||
totalFeaturesWithGeom: totalWithGeom,
|
||||
trigger: "gis_feature_sync_geom (BEFORE INSERT OR UPDATE OF geometry)",
|
||||
index: "gis_feature_geom_idx (GiST)",
|
||||
views: [
|
||||
"gis_features (master)",
|
||||
"gis_terenuri",
|
||||
"gis_cladiri",
|
||||
"gis_documentatii",
|
||||
"gis_administrativ",
|
||||
],
|
||||
qgisConnection: {
|
||||
host: "10.10.10.166",
|
||||
port: 5432,
|
||||
database: "architools_db",
|
||||
user: "architools_user",
|
||||
srid: 3844,
|
||||
note: "Adaugă layere din view-uri: gis_terenuri, gis_cladiri, etc.",
|
||||
},
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Eroare server";
|
||||
console.error("[setup-postgis] Error:", error);
|
||||
return Response.json({ success: false, error: message }, { status: 500 });
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user