gis/api/openapi.yaml

809 lines
25 KiB
YAML

openapi: 3.1.1
info:
title: GIS API
version: "1.0.0"
summary: Hierarchical categories and geo datasets with async processing.
description: |
HTTP API for hierarchical categories and geo datasets (vector,
vector_with_kato, raster).
Datasets are processed asynchronously after upload, dispatched by `file_type`:
- `vector` — the attribute table is extracted into `properties`.
- `raster` — converted to a Cloud-Optimized GeoTIFF; footprint `geometry`
and `bbox` are derived from the raster extent.
- `vector_with_kato` — columns are detected for selection; the client then
submits a KATO/year mapping, unpivoted into observations.
Poll `GET /datasets/{id}/status` (optionally long-polling) to follow progress
through the status lifecycle:
`pending → processing|parsing → awaiting_mapping → extracting → ready`
(or `failed`).
license:
name: Proprietary
servers:
- url: http://localhost:8080
description: Local development
tags:
- name: Health
description: Liveness and readiness probes
- name: Categories
description: Hierarchical dataset categories
- name: Datasets
description: Geo dataset upload, processing, and retrieval
paths:
/healthz:
get:
tags: [Health]
summary: Liveness probe
responses:
"200":
description: Process is alive
content:
application/json:
schema:
type: object
properties:
status:
type: string
/readyz:
get:
tags: [Health]
summary: Readiness probe
description: Runs dependency checks (Postgres, S3/MinIO, RabbitMQ).
responses:
"200":
description: All dependencies reachable
content:
application/json:
schema: { $ref: "#/components/schemas/Readiness" }
"503":
description: One or more dependencies unavailable
content:
application/json:
schema: { $ref: "#/components/schemas/Readiness" }
/categories:
get:
tags: [Categories]
summary: List categories
parameters:
- name: parent_id
in: query
required: false
description: Filter to the direct children of this category.
schema:
type: string
format: uuid
responses:
"200":
description: Categories ordered by name
content:
application/json:
schema:
type: array
items: { $ref: "#/components/schemas/Category" }
"400": { $ref: "#/components/responses/BadRequest" }
post:
tags: [Categories]
summary: Create a category
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CategoryInput" }
responses:
"201":
description: Created
content:
application/json:
schema: { $ref: "#/components/schemas/Category" }
"400": { $ref: "#/components/responses/BadRequest" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/ValidationError" }
/categories/{id}:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Categories]
summary: Get a category
responses:
"200":
description: The category
content:
application/json:
schema: { $ref: "#/components/schemas/Category" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
put:
tags: [Categories]
summary: Update a category
description: |
Updates name, description, and parent. The new parent must exist and must
not create a cycle (a category cannot be its own ancestor).
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/CategoryInput" }
responses:
"200":
description: Updated
content:
application/json:
schema: { $ref: "#/components/schemas/Category" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Categories]
summary: Delete a category
description: Fails with 409 if datasets or child categories still reference it.
responses:
"204": { description: Deleted }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
/datasets:
get:
tags: [Datasets]
summary: List datasets (paginated summaries)
parameters:
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/PageSizeParam"
- name: category_id
in: query
required: false
description: Filter to a category by id.
schema:
type: string
format: uuid
- name: category_code
in: query
required: false
description: >-
Filter to a category by its code (slug). An unknown code yields an
empty page.
schema:
type: string
responses:
"200":
description: A page of dataset summaries
content:
application/json:
schema: { $ref: "#/components/schemas/DatasetSummaryPage" }
"400": { $ref: "#/components/responses/BadRequest" }
post:
tags: [Datasets]
summary: Upload a dataset
description: |
Multipart upload, validated by `file_type`, extension, and a content
magic-byte check, then processed asynchronously (poll
`GET /datasets/{id}/status`).
Allowed extensions: `vector`/`vector_with_kato` = `.geojson`/`.gpkg`/`.zip`;
`raster` = `.tif`/`.tiff`.
requestBody:
required: true
content:
multipart/form-data:
schema:
type: object
required: [file, file_type, category_id]
properties:
file:
type: string
format: binary
description: The geo file.
file_type:
$ref: "#/components/schemas/FileType"
category_id:
type: string
format: uuid
name:
type: string
description: Display name; defaults to the filename if omitted.
description:
type: string
unit:
type: string
meta:
type: string
description: Arbitrary user JSON (sent as a JSON string).
automated:
type: boolean
responses:
"201":
description: Dataset created; processing enqueued
content:
application/json:
schema: { $ref: "#/components/schemas/Dataset" }
"400": { $ref: "#/components/responses/BadRequest" }
"422": { $ref: "#/components/responses/ValidationError" }
/datasets/{id}:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: Get a dataset
description: Full dataset, including geometry as GeoJSON and bbox for rasters.
responses:
"200":
description: The dataset
content:
application/json:
schema: { $ref: "#/components/schemas/Dataset" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
delete:
tags: [Datasets]
summary: Delete a dataset
description: Removes the row and the stored object(s).
responses:
"204": { description: Deleted }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
/datasets/{id}.geojson:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: Get a dataset as GeoJSON (vector / vector_with_kato)
description: |
Assembles a GeoJSON `FeatureCollection` (RFC 7946) from the dataset.
A plain `vector` dataset has no KATO mapping or observations, so the
result is a single Feature wrapping the dataset's own (dissolved)
geometry. Its extracted attribute table (e.g. a GeoPackage's table data,
stored in `properties`) is exposed as the Feature's top-level properties:
a single row becomes the properties object directly, multiple rows are
kept under a `rows` key. An empty collection is returned when the dataset
has no geometry.
A `vector_with_kato` dataset is built from its observations. When it has
its own (dissolved) geometry, the observations are taken to describe that
whole geometry: a single Feature wraps it, and its properties hold only
the observations, keyed by KATO code at the top level (each KATO mapping
to its date->value pairs). Otherwise one Feature is emitted per KATO, its
boundary taken from the `districts` table and the per-year values
flattened into the Feature's properties keyed by date; KATO codes with
no matching district are skipped.
Only `ready` datasets are served: a dataset still being processed
returns 409. Other file types (e.g. `raster`) return 422.
responses:
"200":
description: The dataset as a GeoJSON FeatureCollection
content:
application/geo+json:
schema: { $ref: "#/components/schemas/GeoJSONFeatureCollection" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/ValidationError" }
/datasets/{id}.kato.geojson:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: Get a dataset as district-joined GeoJSON (vector_with_kato)
description: |
Like `/datasets/{id}.geojson`, but ignores any geometry the dataset
carries and always joins the `districts` table on KATO code: one Feature
is emitted per KATO, its boundary taken from the matching district and
the per-year observation values mapped into the Feature's properties
keyed by date (alongside `kato` and `name`). KATO codes with no matching
district are skipped.
Only `ready` datasets are served: a dataset still being processed
returns 409. Other file types return 422.
responses:
"200":
description: The dataset as a district-joined GeoJSON FeatureCollection
content:
application/geo+json:
schema: { $ref: "#/components/schemas/GeoJSONFeatureCollection" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/ValidationError" }
/datasets/{id}/status:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: Get processing status (supports long polling)
description: |
Returns the current status immediately. If `current` is supplied, the
request is held until the status differs from it, or until `wait` seconds
elapse (then the unchanged status is returned).
parameters:
- name: current
in: query
required: false
description: The client's last-known status; enables long polling.
schema: { $ref: "#/components/schemas/DatasetStatus" }
- name: wait
in: query
required: false
description: Max seconds to hold the request (default 25, max 60).
schema:
type: integer
minimum: 0
maximum: 60
default: 25
responses:
"200":
description: The dataset's status
content:
application/json:
schema: { $ref: "#/components/schemas/DatasetStatusInfo" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
/datasets/{id}/download:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: Download the original uploaded file
responses:
"200":
description: The file stream
content:
application/octet-stream:
schema:
type: string
format: binary
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
/datasets/{id}/mapping:
parameters:
- $ref: "#/components/parameters/IdParam"
post:
tags: [Datasets]
summary: Set the KATO column and year mapping (vector_with_kato)
description: |
Valid only for `vector_with_kato` datasets in `awaiting_mapping` (or
`ready`, to re-map). The KATO column and every mapped column must be among
the dataset's detected `attribute_columns`. Moves the dataset to
`extracting`; observations are produced asynchronously.
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/MappingInput" }
responses:
"200":
description: Mapping saved; extraction enqueued
content:
application/json:
schema: { $ref: "#/components/schemas/Dataset" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
"409": { $ref: "#/components/responses/Conflict" }
"422": { $ref: "#/components/responses/ValidationError" }
/datasets/{id}/observations:
parameters:
- $ref: "#/components/parameters/IdParam"
get:
tags: [Datasets]
summary: List a dataset's observations (paginated)
description: Long-format values unpivoted from a vector_with_kato dataset.
parameters:
- name: kato_code
in: query
required: false
description: Filter to a single KATO code.
schema:
type: string
- $ref: "#/components/parameters/PageParam"
- $ref: "#/components/parameters/PageSizeParam"
responses:
"200":
description: A page of observations
content:
application/json:
schema: { $ref: "#/components/schemas/ObservationPage" }
"400": { $ref: "#/components/responses/BadRequest" }
"404": { $ref: "#/components/responses/NotFound" }
components:
parameters:
IdParam:
name: id
in: path
required: true
schema:
type: string
format: uuid
PageParam:
name: page
in: query
required: false
description: 1-based page number.
schema:
type: integer
minimum: 1
default: 1
PageSizeParam:
name: page_size
in: query
required: false
description: Items per page (default 20, max 100).
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
BadRequest:
description: Malformed request (e.g. invalid UUID or query parameter)
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
NotFound:
description: Resource not found
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
Conflict:
description: Operation conflicts with existing data (e.g. foreign key)
content:
application/json:
schema: { $ref: "#/components/schemas/Error" }
ValidationError:
description: Input failed validation
content:
application/json:
schema:
oneOf:
- { $ref: "#/components/schemas/Error" }
- { $ref: "#/components/schemas/ValidationErrors" }
schemas:
Error:
type: object
required: [error]
properties:
error:
type: string
ValidationErrors:
type: object
description: Per-field validation messages.
properties:
errors:
type: object
additionalProperties:
type: string
Readiness:
type: object
properties:
ready:
type: boolean
components:
type: object
additionalProperties:
type: string
FileType:
type: string
enum: [vector_with_kato, vector, raster]
DatasetStatus:
type: string
description: Dataset processing lifecycle status.
enum: [pending, processing, parsing, awaiting_mapping, extracting, ready, failed]
Category:
type: object
required: [id, code, name, description, created_at, updated_at]
properties:
id:
type: string
format: uuid
parent_id:
type: [string, "null"]
format: uuid
code:
type: string
description: Slug (lowercase latin letters, digits, and dashes).
name:
type: string
description:
type: string
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
CategoryInput:
type: object
required: [code, name]
properties:
parent_id:
type: [string, "null"]
format: uuid
code:
type: string
maxLength: 255
pattern: "^[a-z0-9]+(?:-[a-z0-9]+)*$"
description: Slug (lowercase latin letters, digits, and dashes).
name:
type: string
maxLength: 255
description:
type: string
maxLength: 2000
AttributeColumn:
type: object
required: [name]
properties:
name:
type: string
samples:
type: array
items:
type: string
YearColumn:
type: object
required: [column, date]
properties:
column:
type: string
date:
type: string
format: date
GeoJSONGeometry:
type: [object, "null"]
description: A GeoJSON geometry object (e.g. Point, Polygon).
properties:
type:
type: string
coordinates: true
GeoJSONFeature:
type: object
description: A GeoJSON Feature (RFC 7946).
required: [type, geometry, properties]
properties:
type:
type: string
enum: [Feature]
geometry:
$ref: "#/components/schemas/GeoJSONGeometry"
properties:
type: [object, "null"]
description: |
Arbitrary key/value map. For per-KATO features (no dataset geometry)
this holds `kato`, `name`, and one entry per mapped year keyed by
date (YYYY-MM-DD). For the single-feature case (dataset has its own
geometry) it holds only the observations, keyed by KATO code, each
mapping to its date->value pairs.
additionalProperties: true
GeoJSONFeatureCollection:
type: object
description: A GeoJSON FeatureCollection (RFC 7946).
required: [type, features]
properties:
type:
type: string
enum: [FeatureCollection]
features:
type: array
items:
$ref: "#/components/schemas/GeoJSONFeature"
DatasetSummary:
type: object
required: [id, category_id, name, file_type, size_bytes, status, created_at, updated_at]
properties:
id:
type: string
format: uuid
category_id:
type: string
format: uuid
name:
type: string
description:
type: [string, "null"]
unit:
type: [string, "null"]
file_type:
$ref: "#/components/schemas/FileType"
size_bytes:
type: integer
format: int64
status:
$ref: "#/components/schemas/DatasetStatus"
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
Dataset:
type: object
required:
[id, category_id, name, filename, storage_key, file_type,
size_bytes, content_type, automated, status, created_at, updated_at]
properties:
id:
type: string
format: uuid
category_id:
type: string
format: uuid
name:
type: string
description:
type: [string, "null"]
unit:
type: [string, "null"]
filename:
type: string
storage_key:
type: string
cog_storage_key:
type: [string, "null"]
description: Set for rasters once the Cloud-Optimized GeoTIFF is produced.
file_type:
$ref: "#/components/schemas/FileType"
size_bytes:
type: integer
format: int64
content_type:
type: string
properties:
type: [array, "null"]
description: Extracted attribute table (plain vector); rows of key/value.
items:
type: object
additionalProperties:
type: string
meta:
type: [object, "null"]
description: Arbitrary user-supplied JSON.
automated:
type: boolean
status:
$ref: "#/components/schemas/DatasetStatus"
attribute_columns:
type: [array, "null"]
description: Detected columns (vector_with_kato), for mapping.
items:
$ref: "#/components/schemas/AttributeColumn"
kato_column:
type: [string, "null"]
year_columns:
type: [array, "null"]
items:
$ref: "#/components/schemas/YearColumn"
parse_error:
type: [string, "null"]
description: Failure reason when status is `failed`.
geometry:
$ref: "#/components/schemas/GeoJSONGeometry"
bbox:
type: array
description: "[minX, minY, maxX, maxY]; present only for rasters."
items:
type: number
minItems: 4
maxItems: 4
created_at:
type: string
format: date-time
updated_at:
type: string
format: date-time
MappingInput:
type: object
required: [kato_column, year_columns]
properties:
kato_column:
type: string
examples: ["като"]
year_columns:
type: array
minItems: 1
items:
$ref: "#/components/schemas/YearColumn"
examples:
- kato_column: "като"
year_columns:
- { column: F_2023, date: "2023-01-01" }
- { column: D_2025, date: "2025-01-01" }
Observation:
type: object
required: [id, dataset_id, kato_code, date]
properties:
id:
type: string
format: uuid
dataset_id:
type: string
format: uuid
kato_code:
type: string
date:
type: string
format: date
value:
type: [number, "null"]
description: Numeric cell value, or null when non-numeric/empty.
value_text:
type: [string, "null"]
description: Non-numeric cell value, or null.
DatasetStatusInfo:
type: object
required: [id, status]
properties:
id:
type: string
format: uuid
status:
$ref: "#/components/schemas/DatasetStatus"
parse_error:
type: [string, "null"]
DatasetSummaryPage:
type: object
required: [data, page, page_size, total, total_pages]
properties:
data:
type: array
items:
$ref: "#/components/schemas/DatasetSummary"
page:
type: integer
page_size:
type: integer
total:
type: integer
total_pages:
type: integer
ObservationPage:
type: object
required: [data, page, page_size, total, total_pages]
properties:
data:
type: array
items:
$ref: "#/components/schemas/Observation"
page:
type: integer
page_size:
type: integer
total:
type: integer
total_pages:
type: integer