feat(industrial): add OSM provenance + sync state to IndustrialPark (PR 1/4)
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 4s
CI / AI Services (Python) — Smoke (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 48s
Deploy / Build API Image (push) Failing after 5s
Deploy / Build Web Image (push) Failing after 4s
Deploy / Build AI Services Image (push) Failing after 5s
E2E Tests / Playwright E2E (push) Failing after 7s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 31s
Security Scanning / Trivy Scan — Web Image (push) Failing after 40s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 40s
Security Scanning / Trivy Filesystem Scan (push) Failing after 25s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
Some checks failed
CI / Lint → Typecheck → Test → Build (22) (push) Failing after 4s
CI / AI Services (Python) — Smoke (push) Failing after 4s
CI / E2E Tests (push) Has been skipped
CodeQL Analysis / CodeQL (javascript-typescript) (push) Failing after 48s
Deploy / Build API Image (push) Failing after 5s
Deploy / Build Web Image (push) Failing after 4s
Deploy / Build AI Services Image (push) Failing after 5s
E2E Tests / Playwright E2E (push) Failing after 7s
Security Scanning / Dependency Audit (pnpm) (push) Failing after 3s
Security Scanning / Trivy Scan — API Image (push) Failing after 31s
Security Scanning / Trivy Scan — Web Image (push) Failing after 40s
Security Scanning / Trivy Scan — AI Services Image (push) Failing after 40s
Security Scanning / Trivy Filesystem Scan (push) Failing after 25s
Deploy / Deploy to Staging (push) Has been skipped
Deploy / Smoke Test Staging (push) Has been skipped
Deploy / Deploy to Production (push) Has been skipped
Deploy / Smoke Test Production (push) Has been skipped
Security Scanning / Security Gate (push) Failing after 1s
Deploy / Rollback Staging (push) Has been skipped
Deploy / Rollback Production (push) Has been skipped
First PR of the OSM-sync project. Adds the schema scaffolding so the
follow-up bulk-import PR can write OSM-sourced rows alongside the 50
hand-curated industrial parks already in the table without disturbing
public list/detail/map flows or the IndustrialListing FK relationship.
New enums:
- IndustrialParkOsmType: NODE | WAY | RELATION
- IndustrialParkDataSource:
MANUAL existing curated rows (default for the 50 backfilled)
OSM raw OSM import, hidden from public until promoted
OSM_PROMOTED admin-reviewed OSM row visible on the public list
New columns on IndustrialPark:
- dataSource — drives public visibility + sync policy
- isPublic — true for MANUAL, false for raw OSM
- osmType, osmId — link to OSM entity (osmId UNIQUE)
- osmVersion, osmTags — incremental sync state + raw tag bag (JSONB)
- boundary — PostGIS MultiPolygon for park outline (Point
centroid stays in `location` for low-zoom render)
- osmLocked — admin freeze flag; sync skips this row entirely
- lockedFields — per-field freeze list; sync preserves listed cols
- lastSyncedAt — last reconcile pass timestamp
New indexes:
- osmId — sync upsert lookup
- (dataSource, isPublic) — public list filter
- boundary GiST — viewport / bbox spatial queries
- lastSyncedAt — cron staleness scan
Backfill behaviour: existing 20 rows automatically get
dataSource=MANUAL, isPublic=true via column defaults — no breaking
change for current consumers (frontend list, detail, map, admin
moderation, IndustrialListing FK).
Manually written migration SQL because Prisma cannot manage the
PostGIS Geometry type — `boundary` is added via AddGeometryColumn().
Next PRs:
- PR 2: bulk-import script (Overpass + osmium fallback)
- PR 3: bbox spatial API + frontend Mapbox layer (cluster + outlines)
- PR 4: monthly sync cron + admin diff/promote UI
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1083,6 +1083,27 @@ enum IndustrialParkStatus {
|
||||
FULL
|
||||
}
|
||||
|
||||
/// OSM element type — way/relation are most common for industrial parks
|
||||
/// (polygon boundaries), node only used when the park has no traced area.
|
||||
enum IndustrialParkOsmType {
|
||||
NODE
|
||||
WAY
|
||||
RELATION
|
||||
}
|
||||
|
||||
/// Provenance of an IndustrialPark row. Used to filter what's shown on the
|
||||
/// public KCN list (only MANUAL + OSM_PROMOTED) versus the admin queue
|
||||
/// (everything, including raw OSM imports).
|
||||
enum IndustrialParkDataSource {
|
||||
/// Human-curated by goodgo team; full business data (rents, fees, media).
|
||||
MANUAL
|
||||
/// Imported from OpenStreetMap, not yet vetted. Hidden from public list.
|
||||
OSM
|
||||
/// Imported from OSM and reviewed by an admin who promoted it to the
|
||||
/// public catalog. Geometry/name still tracked against OSM via osmId.
|
||||
OSM_PROMOTED
|
||||
}
|
||||
|
||||
enum IndustrialPropertyType {
|
||||
INDUSTRIAL_LAND
|
||||
READY_BUILT_FACTORY
|
||||
@@ -1150,6 +1171,39 @@ model IndustrialPark {
|
||||
/// Optional owning operator user (role=PARK_OPERATOR). NULL for parks not
|
||||
/// yet assigned to an operator account — admin still manages those.
|
||||
ownerId String?
|
||||
|
||||
// ─── OSM provenance & sync state ─────────────────────────────────────────
|
||||
/// Marker for where this row came from. Drives public visibility +
|
||||
/// conflict-resolution policy during OSM sync.
|
||||
dataSource IndustrialParkDataSource @default(MANUAL)
|
||||
/// Hidden from the public list when false. OSM-imported rows default to
|
||||
/// false until an admin promotes them; MANUAL rows default to true.
|
||||
isPublic Boolean @default(true)
|
||||
/// OpenStreetMap entity that this row mirrors (NULL for purely manual rows).
|
||||
/// `osmId` is unique because OSM ids are scoped per-type, but in practice
|
||||
/// most industrial parks are `way` so collisions are vanishingly rare.
|
||||
osmType IndustrialParkOsmType?
|
||||
osmId BigInt? @unique
|
||||
/// OSM `version` tag. Used during incremental sync to detect remote edits.
|
||||
osmVersion Int?
|
||||
/// Full OSM tag bag, kept as JSONB for flexibility (we don't model every
|
||||
/// possible tag — operator, website, addr:*, source, etc.).
|
||||
osmTags Json?
|
||||
/// Polygon outline of the park as a MultiPolygon. NULL when the OSM entity
|
||||
/// is a node (no traced area) or when sourced from a manual seed without
|
||||
/// boundary tracing. `location` (Point) remains the centroid for low-zoom
|
||||
/// rendering.
|
||||
boundary Unsupported("geometry(MultiPolygon, 4326)")?
|
||||
/// When true the OSM sync cron skips this row entirely (admin froze it).
|
||||
/// Useful for parks where OSM tag noise would overwrite curated data.
|
||||
osmLocked Boolean @default(false)
|
||||
/// Per-field lock list. Even when `osmLocked = false`, the sync cron
|
||||
/// preserves any column whose name appears here. Lets admins fix one
|
||||
/// field (e.g. `name`) without freezing the whole row.
|
||||
lockedFields String[] @default([])
|
||||
/// Last successful Overpass/PBF reconcile pass; NULL means never synced.
|
||||
lastSyncedAt DateTime?
|
||||
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@ -1167,6 +1221,11 @@ model IndustrialPark {
|
||||
@@index([region, province, status])
|
||||
@@index([createdAt])
|
||||
@@index([ownerId])
|
||||
// OSM sync access patterns
|
||||
@@index([osmId])
|
||||
@@index([dataSource, isPublic])
|
||||
@@index([boundary], type: Gist)
|
||||
@@index([lastSyncedAt])
|
||||
}
|
||||
|
||||
model IndustrialListing {
|
||||
|
||||
Reference in New Issue
Block a user