openapi: 3.1.0
info:
  title: GeoVault API
  version: 1.0.0
  description: |
    Registered GeoVault API/data routes. Paths have no /geovault prefix. A reverse
    proxy often mounts the service there; BASE_PATH and X-Forwarded-Prefix may apply.

    All operations use RequireLogin and RequireGeoVaultServiceAccess. Cookie
    acugis_session is operational. Shared auth attempts bearer PAT authentication,
    but GeoVault has no TokenStore, so bearer PATs are currently unauthenticated.
    There is no JWT, OAuth, API-key authentication, or CORS support.

    service/geovault requires view for GET/HEAD and edit for mutations; /connections*,
    /api/connections*, and /api/database-services* are intended to require admin.
    Classification checks unprefixed paths, so BASE_PATH deployments can degrade
    some intended admin checks. Dataset-aware handlers additionally enforce
    geovault_dataset view/edit/admin.

    GeoVault sets User-Agent GeoVault/1.0 for outbound URL imports only; clients
    are not required to send that header.

    QGIS Publish Service and PostGIS Service passthrough payloads are upstream-owned.
    Where their source contract is unavailable, this document intentionally leaves
    the JSON schema open instead of inventing fields.
servers:
  - url: http://127.0.0.1:8085
    description: Direct GeoVault listener
  - url: /geovault
    description: Common reverse-proxy mount
security:
  - cookieAuth: []
tags:
  - {name: Folders}
  - {name: QGIS projects}
  - {name: Import}
  - {name: Datasets}
  - {name: Versions}
  - {name: Processing}
  - {name: Dataset editing}
  - {name: PostgreSQL}
  - {name: Database services}
  - {name: PostGIS service}
  - {name: ESRI}
  - {name: GeoServer}

paths:
  /api/folders:
    get:
      tags: [Folders]
      summary: List visible folders
      parameters:
        - {name: archived, in: query, schema: {type: string, const: only}}
      responses:
        "200": {description: Folder summaries, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/Folder"}}}}}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [Folders]
      summary: Create a folder
      requestBody: {$ref: "#/components/requestBodies/CreateFolder"}
      responses:
        "201": {description: Created folder, content: {application/json: {schema: {$ref: "#/components/schemas/Folder"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/folders/{id}:
    parameters:
      - {$ref: "#/components/parameters/FolderId"}
    get:
      tags: [Folders]
      summary: Get a folder
      responses:
        "200": {description: Folder, content: {application/json: {schema: {$ref: "#/components/schemas/Folder"}}}}
        default: {$ref: "#/components/responses/Error"}
    put:
      tags: [Folders]
      summary: Update a folder
      requestBody: {$ref: "#/components/requestBodies/UpdateFolder"}
      responses:
        "200": {description: Updated folder, content: {application/json: {schema: {$ref: "#/components/schemas/Folder"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/folders/{id}/archive:
    parameters:
      - {$ref: "#/components/parameters/FolderId"}
    post:
      tags: [Folders]
      summary: Archive a folder
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}

  /api/qgis-projects:
    get:
      tags: [QGIS projects]
      summary: List published QGIS projects
      responses:
        "200": {description: Project summaries, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/QGISProject"}}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/qgis-projects/{id}:
    parameters:
      - {$ref: "#/components/parameters/StringId"}
    get:
      tags: [QGIS projects]
      summary: Get a QGIS project
      responses:
        "200": {description: Project summary, content: {application/json: {schema: {$ref: "#/components/schemas/QGISProject"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/qgis-projects/{id}/versions:
    parameters:
      - {$ref: "#/components/parameters/StringId"}
    get:
      tags: [QGIS projects]
      summary: List QGIS project runtime and snapshot versions
      responses:
        "200": {description: Project versions, content: {application/json: {schema: {$ref: "#/components/schemas/QGISVersions"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/qgis-projects/{id}/versions/{version}/restore:
    parameters:
      - {$ref: "#/components/parameters/StringId"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [QGIS projects]
      summary: Restore a QGIS project snapshot
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/qgis-projects/{id}/download:
    parameters:
      - {$ref: "#/components/parameters/StringId"}
    get:
      tags: [QGIS projects]
      summary: Download current QGIS project
      responses:
        "200": {$ref: "#/components/responses/Binary"}
        default: {$ref: "#/components/responses/Error"}
  /api/qgis-projects/{id}/versions/{version}/download:
    parameters:
      - {$ref: "#/components/parameters/StringId"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [QGIS projects]
      summary: Download a QGIS project snapshot
      responses:
        "200": {$ref: "#/components/responses/Binary"}
        default: {$ref: "#/components/responses/Error"}

  /import-url:
    post:
      tags: [Import]
      summary: Import a dataset from a URL
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/ImportURL"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /import-s3:
    post:
      tags: [Import]
      summary: Import or remotely register an S3 object
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/ImportS3"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /projects/{id}/upload:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    post:
      tags: [Import]
      summary: Upload a project dataset version
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/Upload"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        "303": {description: Browser redirect to the dataset page, headers: {Location: {schema: {type: string}}}}
        default: {$ref: "#/components/responses/Error"}
  /projects/{id}/versions:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [Versions]
      summary: List visible project versions
      responses:
        "200": {description: Version array, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/Version"}}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/discovery:
    get:
      tags: [Datasets]
      summary: Return federation discovery items
      responses:
        "200": {description: Discovery response, content: {application/json: {schema: {$ref: "#/components/schemas/Discovery"}}}}
        default: {$ref: "#/components/responses/Error"}

  /datasets:
    get:
      tags: [Datasets]
      summary: List visible datasets
      description: Returns all ACL-visible dataset names. This handler ignores folder_id and every other query parameter.
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets-summary:
    get:
      tags: [Datasets]
      summary: List detailed dataset summaries
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
        - {name: type, in: query, description: Exact case-insensitive match to the recorded dataset type, schema: {type: string, examples: [vector]}}
        - {name: archived, in: query, description: Active only by default; only/true/1 selects archived only; include/all selects both, schema: {type: string, enum: [only, "true", "1", include, all]}}
      responses:
        "200": {description: Dataset summaries, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/DatasetSummary"}}}}}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    get:
      tags: [Datasets]
      summary: Render dataset detail page
      responses:
        "200": {description: Dataset HTML, content: {text/html: {schema: {type: string}}}}
        default: {$ref: "#/components/responses/Error"}
    delete:
      tags: [Datasets]
      summary: Permanently delete a confirmed archived dataset
      description: Requires geovault_dataset admin. When metadata DB is configured the dataset must be archived.
      parameters:
        - {name: confirm, in: query, required: true, schema: {type: boolean, const: true}}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/upload:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    post:
      tags: [Import]
      summary: Upload a named dataset version
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/Upload"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/FolderIdQuery"}
    get:
      tags: [Versions]
      summary: List dataset versions
      responses:
        "200": {description: Versions, content: {application/json: {schema: {type: object, properties: {versions: {type: array, items: {$ref: "#/components/schemas/Version"}}}}}}}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    delete:
      tags: [Versions]
      summary: Delete a non-latest dataset version
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/rename:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    post:
      tags: [Datasets]
      summary: Rename a dataset
      requestBody: {$ref: "#/components/requestBodies/Rename"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/archive:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    post:
      tags: [Datasets]
      summary: Archive a dataset
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/restore:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    post:
      tags: [Datasets]
      summary: Restore an archived dataset
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/clone:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    post:
      tags: [Datasets]
      summary: Clone a dataset
      requestBody: {$ref: "#/components/requestBodies/Clone"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/metadata:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    get:
      tags: [Datasets]
      summary: Read user-maintained dataset metadata
      responses:
        "200": {description: Dataset metadata, content: {application/json: {schema: {$ref: "#/components/schemas/UserMetadata"}}}}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [Datasets]
      summary: Save user-maintained dataset metadata
      requestBody: {$ref: "#/components/requestBodies/UserMetadata"}
      responses:
        "200": {description: Saved metadata, content: {application/json: {schema: {$ref: "#/components/schemas/UserMetadata"}}}}
        default: {$ref: "#/components/responses/Error"}

  /versions/{id}/publish:
    parameters:
      - {$ref: "#/components/parameters/IgnoredPublishPathId"}
    post:
      tags: [Versions]
      summary: Publish a version through the unified target
      description: The routed path id is ignored. The required body version_id selects the version.
      requestBody: {$ref: "#/components/requestBodies/Publish"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/versions/{id}/publish-capability:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [Versions]
      summary: Get version publish capability
      responses:
        "200": {description: Capability, content: {application/json: {schema: {$ref: "#/components/schemas/PublishCapability"}}}}
        default: {$ref: "#/components/responses/Error"}
  /versions/{id}/lineage:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [Versions]
      summary: Get version lineage
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /versions/{id}/rollback:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    post:
      tags: [Versions]
      summary: Roll back by creating a new version
      requestBody:
        required: false
        content:
          application/json:
            schema:
              type: object
              properties:
                reason: {type: string, description: Defaults to "Rollback to v<source>"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /versions/{id}/download:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [Versions]
      summary: Download a local version file
      responses:
        "200": {$ref: "#/components/responses/Binary"}
        default: {$ref: "#/components/responses/Error"}
  /metadata:
    get:
      tags: [Versions]
      summary: Get or extract technical metadata
      parameters:
        - {$ref: "#/components/parameters/DatasetQuery"}
        - {$ref: "#/components/parameters/VersionQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /preview:
    get:
      tags: [Versions]
      summary: Preview a dataset version
      description: Vector and PostGIS previews return GeoJSON. Raster previews return image/jpeg or image/png.
      parameters:
        - {$ref: "#/components/parameters/DatasetQuery"}
        - {$ref: "#/components/parameters/VersionQuery"}
        - {$ref: "#/components/parameters/LayerQuery"}
      responses:
        "200":
          description: GeoJSON FeatureCollection or raster image
          content:
            application/json: {schema: {$ref: "#/components/schemas/FeatureCollection"}}
            image/jpeg: {schema: {type: string, format: binary}}
            image/png: {schema: {type: string, format: binary}}
        default: {$ref: "#/components/responses/Error"}
  /register:
    post:
      tags: [Datasets]
      summary: Compatibility stub
      description: Body is ignored and no registration is performed.
      responses:
        "200": {description: Empty response}
        default: {$ref: "#/components/responses/Error"}

  /datasets/{name}/versions/{version}/fields:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [Processing]
      summary: List layer fields
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/values:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [Processing]
      summary: List distinct field values
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
        - {name: field, in: query, required: true, schema: {type: string}}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/filter:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Processing]
      summary: Create a filtered version
      requestBody: {$ref: "#/components/requestBodies/Filter"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/dissolve:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Processing]
      summary: Create a dissolved version
      requestBody: {$ref: "#/components/requestBodies/Dissolve"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/clip:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Processing]
      summary: Create a clipped version
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/Clip"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /datasets/{name}/versions/{version}/edit/capability:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [Dataset editing]
      summary: Get edit capability
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/edit/schema:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [Dataset editing]
      summary: Get editor schema
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      responses:
        "200": {description: Editor schema, content: {application/json: {schema: {$ref: "#/components/schemas/EditSchema"}}}}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/edit/rows:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    get:
      tags: [Dataset editing]
      summary: List editor rows
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
        - {$ref: "#/components/parameters/Limit"}
        - {$ref: "#/components/parameters/Offset"}
        - {$ref: "#/components/parameters/Sort"}
        - {$ref: "#/components/parameters/Order"}
        - {$ref: "#/components/parameters/Filters"}
      responses:
        "200": {description: Rows, content: {application/json: {schema: {$ref: "#/components/schemas/Rows"}}}}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/edit/update:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Dataset editing]
      summary: Update a dataset row
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      requestBody: {$ref: "#/components/requestBodies/EditUpdate"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/edit/delete:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Dataset editing]
      summary: Delete a dataset row
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      requestBody: {$ref: "#/components/requestBodies/EditDelete"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /datasets/{name}/versions/{version}/edit/insert:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
      - {$ref: "#/components/parameters/VersionPath"}
    post:
      tags: [Dataset editing]
      summary: Insert a dataset row
      parameters:
        - {$ref: "#/components/parameters/LayerQuery"}
      requestBody: {$ref: "#/components/requestBodies/EditInsert"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /api/connections/test:
    post:
      tags: [PostgreSQL]
      summary: Test PostgreSQL/PostGIS connectivity
      requestBody: {$ref: "#/components/requestBodies/TestConnection"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/connections:
    get:
      tags: [PostgreSQL]
      summary: List saved PostgreSQL connections
      responses:
        "200": {description: Public connections, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/PgConnectionPublic"}}}}}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [PostgreSQL]
      summary: Create a PostgreSQL connection
      requestBody: {$ref: "#/components/requestBodies/PgConnectionCreate"}
      responses:
        "201": {$ref: "#/components/responses/Id"}
        default: {$ref: "#/components/responses/Error"}
  /api/connections/{id}:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    put:
      tags: [PostgreSQL]
      summary: Update a PostgreSQL connection
      requestBody: {$ref: "#/components/requestBodies/PgConnectionUpdate"}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
    delete:
      tags: [PostgreSQL]
      summary: Delete a PostgreSQL connection
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/connections/{id}/tables:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [PostgreSQL]
      summary: List spatial tables
      responses:
        "200": {description: Spatial tables, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/SpatialTable"}}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/connections/{id}/table/{table}/schema:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
      - {$ref: "#/components/parameters/TablePath"}
    get:
      tags: [PostgreSQL]
      summary: Get a PostGIS table schema
      parameters:
        - {$ref: "#/components/parameters/SchemaQuery"}
      responses:
        "200": {description: Editor schema, content: {application/json: {schema: {$ref: "#/components/schemas/EditSchema"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/connections/{id}/table/{table}/rows:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
      - {$ref: "#/components/parameters/TablePath"}
    get:
      tags: [PostgreSQL]
      summary: List PostGIS table rows
      parameters:
        - {$ref: "#/components/parameters/SchemaQuery"}
        - {$ref: "#/components/parameters/Limit"}
        - {$ref: "#/components/parameters/Offset"}
        - {$ref: "#/components/parameters/Sort"}
        - {$ref: "#/components/parameters/Order"}
        - {$ref: "#/components/parameters/Filters"}
      responses:
        "200": {description: Rows, content: {application/json: {schema: {$ref: "#/components/schemas/Rows"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/connections/{id}/table/{table}/preview:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
      - {$ref: "#/components/parameters/TablePath"}
    get:
      tags: [PostgreSQL]
      summary: Preview a PostGIS table as GeoJSON
      parameters:
        - {$ref: "#/components/parameters/SchemaQuery"}
        - {name: geometry_column, in: query, schema: {type: string}}
      responses:
        "200": {$ref: "#/components/responses/GeoJSON"}
        default: {$ref: "#/components/responses/Error"}
  /api/table/update:
    post:
      tags: [PostgreSQL]
      summary: Update a PostGIS table row
      requestBody: {$ref: "#/components/requestBodies/TableUpdate"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/table/delete:
    post:
      tags: [PostgreSQL]
      summary: Delete a PostGIS table row
      requestBody: {$ref: "#/components/requestBodies/TableDelete"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/table/insert:
    post:
      tags: [PostgreSQL]
      summary: Insert a PostGIS table row
      requestBody: {$ref: "#/components/requestBodies/TableInsert"}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/datasets/register:
    post:
      tags: [PostgreSQL]
      summary: Register a PostGIS table as a dataset
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/RegisterPostGIS"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /api/database-services:
    get:
      tags: [Database services]
      summary: List database services without passwords
      responses:
        "200": {description: Services, content: {application/json: {schema: {type: array, items: {$ref: "#/components/schemas/DatabaseService"}}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/database-services/{name}/open:
    parameters:
      - {name: name, in: path, required: true, schema: {type: string}}
    post:
      tags: [Database services]
      summary: Open and synchronize a database service
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /api/postgis-service/connections:
    get:
      tags: [PostGIS service]
      summary: Proxy registry connections
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/databases:
    get:
      tags: [PostGIS service]
      summary: Proxy registry databases
      parameters:
        - {name: connection_id, in: query, schema: {type: integer}}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [PostGIS service]
      summary: Proxy database creation
      description: JSON body is passed through; GeoVault does not define its fields.
      requestBody:
        required: true
        content:
          application/json:
            schema: {type: object, additionalProperties: true}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        "201": {$ref: "#/components/responses/Json"}
        "202": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/discover:
    get:
      tags: [PostGIS service]
      summary: Discover registry databases
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/adopt:
    post:
      tags: [PostGIS service]
      summary: Adopt a registry database
      requestBody: {$ref: "#/components/requestBodies/AdoptPostGIS"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/connections/{id}/pg-users:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [PostGIS service]
      summary: Proxy PostgreSQL users
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/databases/{id}/schemas:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [PostGIS service]
      summary: Proxy database schemas
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/postgis-service/databases/{id}/tables:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [PostGIS service]
      summary: Proxy database tables
      parameters:
        - {$ref: "#/components/parameters/SchemaQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /api/quick-import:
    post:
      tags: [Import]
      summary: Import directly into PostGIS
      requestBody: {$ref: "#/components/requestBodies/QuickImport"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/esri/service/browse:
    post:
      tags: [ESRI]
      summary: Browse an ESRI service
      requestBody: {$ref: "#/components/requestBodies/ESRIBrowse"}
      responses:
        "200": {description: ESRI service and token, content: {application/json: {schema: {$ref: "#/components/schemas/ESRIBrowseResponse"}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/esri/import:
    post:
      tags: [ESRI]
      summary: Import ESRI layers
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/ESRIImport"}
      responses:
        "202": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/esri/jobs/{id}:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [ESRI]
      summary: Get ESRI import job
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

  /api/geoserver/connections:
    get:
      tags: [GeoServer]
      summary: List GeoServer connections
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [GeoServer]
      summary: Create a GeoServer connection
      requestBody: {$ref: "#/components/requestBodies/GeoServerConnectionCreate"}
      responses:
        "201": {$ref: "#/components/responses/Id"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/connections/{id}:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    put:
      tags: [GeoServer]
      summary: Update a GeoServer connection
      requestBody: {$ref: "#/components/requestBodies/GeoServerConnectionUpdate"}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
    delete:
      tags: [GeoServer]
      summary: Delete a GeoServer connection
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/connections/{id}/test:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    post:
      tags: [GeoServer]
      summary: Test a GeoServer connection
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/workspaces:
    get:
      tags: [GeoServer]
      summary: List GeoServer workspaces
      parameters:
        - {$ref: "#/components/parameters/ConnectionIdQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [GeoServer]
      summary: Create a GeoServer workspace
      requestBody: {$ref: "#/components/requestBodies/GeoServerWorkspace"}
      responses:
        "201": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/workspaces/{name}:
    parameters:
      - {name: name, in: path, required: true, schema: {type: string}}
    delete:
      tags: [GeoServer]
      summary: Delete a GeoServer workspace
      parameters:
        - {$ref: "#/components/parameters/ConnectionIdQuery"}
        - {name: recurse, in: query, schema: {type: boolean}}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/catalog:
    get:
      tags: [GeoServer]
      summary: Get GeoServer catalog
      parameters:
        - {$ref: "#/components/parameters/ConnectionIdQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/catalog/import:
    post:
      tags: [GeoServer]
      summary: Import a GeoServer catalog layer
      parameters:
        - {$ref: "#/components/parameters/FolderIdQuery"}
      requestBody: {$ref: "#/components/requestBodies/GeoServerCatalogImport"}
      responses:
        "201": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/styles:
    get:
      tags: [GeoServer]
      summary: List GeoServer styles
      parameters:
        - {$ref: "#/components/parameters/ConnectionIdQuery"}
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
    post:
      tags: [GeoServer]
      summary: Upload an SLD style
      requestBody: {$ref: "#/components/requestBodies/GeoServerStyle"}
      responses:
        "201": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/styles/assign:
    post:
      tags: [GeoServer]
      summary: Assign a style to a layer
      requestBody: {$ref: "#/components/requestBodies/GeoServerStyleAssign"}
      responses:
        "200": {$ref: "#/components/responses/Success"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/styles/{name}:
    parameters:
      - {name: name, in: path, required: true, schema: {type: string}}
    get:
      tags: [GeoServer]
      summary: Download style SLD
      parameters:
        - {$ref: "#/components/parameters/ConnectionIdQuery"}
      responses:
        "200": {description: SLD XML, content: {application/xml: {schema: {type: string}}}}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/publish:
    post:
      tags: [GeoServer]
      summary: Publish an existing version to GeoServer
      requestBody: {$ref: "#/components/requestBodies/GeoServerPublish"}
      responses:
        "202": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/geoserver/jobs/{id}:
    parameters:
      - {$ref: "#/components/parameters/IntegerId"}
    get:
      tags: [GeoServer]
      summary: Get GeoServer publish job
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}
  /api/datasets/{name}/geoserver-publications:
    parameters:
      - {$ref: "#/components/parameters/DatasetName"}
    get:
      tags: [GeoServer]
      summary: List dataset GeoServer publications
      responses:
        "200": {$ref: "#/components/responses/Json"}
        default: {$ref: "#/components/responses/Error"}

components:
  securitySchemes:
    cookieAuth:
      type: apiKey
      in: cookie
      name: acugis_session
      description: Operational GeoVault session authentication.
  parameters:
    IntegerId: {name: id, in: path, required: true, schema: {type: integer, minimum: 1}}
    IgnoredPublishPathId: {name: id, in: path, required: true, description: Required by the route template but ignored by the handler; version_id in the JSON body selects the version, schema: {type: string}}
    StringId: {name: id, in: path, required: true, schema: {type: string}}
    FolderId: {name: id, in: path, required: true, schema: {type: integer, format: int64, minimum: 1}}
    FolderIdQuery: {name: folder_id, in: query, required: false, schema: {type: integer, format: int64, minimum: 1}}
    DatasetName: {name: name, in: path, required: true, schema: {type: string}}
    DatasetQuery: {name: dataset, in: query, required: true, schema: {type: string}}
    VersionPath: {name: version, in: path, required: true, schema: {type: integer, minimum: 1}}
    VersionQuery: {name: version, in: query, required: true, schema: {type: integer, minimum: 1}}
    LayerQuery: {name: layer, in: query, required: false, schema: {type: string}}
    TablePath: {name: table, in: path, required: true, schema: {type: string}}
    SchemaQuery: {name: schema, in: query, required: false, schema: {type: string, default: public}}
    ConnectionIdQuery: {name: connection_id, in: query, required: true, schema: {type: integer, minimum: 1}}
    Limit: {name: limit, in: query, schema: {type: integer, minimum: 1, maximum: 500, default: 50}}
    Offset: {name: offset, in: query, schema: {type: integer, minimum: 0, default: 0}}
    Sort: {name: sort, in: query, schema: {type: string}}
    Order: {name: order, in: query, schema: {type: string, enum: [asc, desc], default: asc}}
    Filters: {name: filters, in: query, description: JSON-encoded array of FilterClause objects, schema: {type: string}}

  responses:
    Json:
      description: JSON response
      content: {application/json: {schema: {type: object, additionalProperties: true}}}
    JsonOrHtml:
      description: JSON for API clients or an HTML redirect/response
      content:
        application/json: {schema: {type: object, additionalProperties: true}}
        text/html: {schema: {type: string}}
    Success:
      description: Successful mutation
      content: {application/json: {schema: {$ref: "#/components/schemas/Success"}}}
    Id:
      description: Created identifier
      content: {application/json: {schema: {type: object, required: [id], properties: {id: {type: integer}}}}}
    Error:
      description: Error response; common statuses are 400, 401, 403, 404, 409, 422, 500, 502, and 503
      content:
        application/json: {schema: {$ref: "#/components/schemas/Error"}}
        text/plain: {schema: {type: string}}
    Binary:
      description: Binary download
      content: {application/octet-stream: {schema: {type: string, format: binary}}}
    GeoJSON:
      description: GeoJSON FeatureCollection
      content: {application/json: {schema: {$ref: "#/components/schemas/FeatureCollection"}}}

  requestBodies:
    CreateFolder:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/CreateFolder"}}}
    UpdateFolder:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/UpdateFolder"}}}
    Upload:
      required: true
      content: {multipart/form-data: {schema: {$ref: "#/components/schemas/Upload"}}}
    ImportURL:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/ImportURL"}}}
    ImportS3:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/ImportS3"}}}
    Rename:
      required: true
      content: {application/json: {schema: {type: object, required: [name], properties: {name: {type: string}}}}}
    Clone:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/Clone"}}}
    UserMetadata:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/UserMetadataInput"}}}
    Publish:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/Publish"}}}
    Filter:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/Filter"}}}
    Dissolve:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/Dissolve"}}}
    Clip:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/Clip"}}}
    EditUpdate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/EditUpdate"}}}
    EditDelete:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/EditDelete"}}}
    EditInsert:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/EditInsert"}}}
    TestConnection:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/TestConnection"}}}
    PgConnectionCreate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/PgConnectionCreate"}}}
    PgConnectionUpdate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/PgConnectionUpdate"}}}
    TableUpdate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/TableUpdate"}}}
    TableDelete:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/TableDelete"}}}
    TableInsert:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/TableInsert"}}}
    RegisterPostGIS:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/RegisterPostGIS"}}}
    AdoptPostGIS:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/AdoptPostGIS"}}}
    QuickImport:
      required: true
      content:
        application/json: {schema: {$ref: "#/components/schemas/QuickImport"}}
        multipart/form-data: {schema: {$ref: "#/components/schemas/QuickImportUpload"}}
    ESRIBrowse:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/ESRIBrowse"}}}
    ESRIImport:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/ESRIImport"}}}
    GeoServerConnectionCreate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerConnectionCreate"}}}
    GeoServerConnectionUpdate:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerConnectionUpdate"}}}
    GeoServerWorkspace:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerWorkspace"}}}
    GeoServerCatalogImport:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerCatalogImport"}}}
    GeoServerStyle:
      required: true
      content:
        application/json: {schema: {$ref: "#/components/schemas/GeoServerStyleJSON"}}
        multipart/form-data: {schema: {$ref: "#/components/schemas/GeoServerStyleMultipart"}}
    GeoServerStyleAssign:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerStyleAssign"}}}
    GeoServerPublish:
      required: true
      content: {application/json: {schema: {$ref: "#/components/schemas/GeoServerPublish"}}}

  schemas:
    Success:
      type: object
      required: [success]
      properties: {success: {type: boolean}}
    Error:
      type: object
      properties:
        error: {type: string}
        details: {type: string}
      additionalProperties: true
    Folder:
      type: object
      required: [id, name, dataset_count, permissions]
      properties:
        id: {type: integer, format: int64}
        name: {type: string}
        description: {type: string}
        owner: {type: string}
        owner_id: {type: integer, format: int64}
        group_name: {type: string}
        dataset_count: {type: integer}
        permissions: {type: array, items: {type: string}}
        updated_at: {type: string}
        archived: {type: boolean}
        archived_at: {type: string}
    CreateFolder:
      type: object
      required: [name]
      properties: {name: {type: string}, description: {type: string}, group_name: {type: string}}
    UpdateFolder:
      type: object
      required: [name]
      properties: {name: {type: string}, description: {type: string}}
    QGISProject:
      type: object
      required: [id, name, snapshot_count, runtime_status]
      properties:
        id: {type: string}
        name: {type: string}
        updated_at: {type: string}
        created_at: {type: string}
        render_mode: {type: string}
        snapshot_count: {type: integer}
        runtime_status: {type: string}
        viewer_url: {type: string}
        can_edit: {type: boolean}
    QGISVersion:
      type: object
      required: [version, created_at]
      properties: {version: {type: integer}, created_at: {type: string}, size_bytes: {type: integer, format: int64}, is_runtime: {type: boolean}}
    QGISVersions:
      type: object
      required: [project_id, versions]
      properties: {project_id: {type: string}, versions: {type: array, items: {$ref: "#/components/schemas/QGISVersion"}}}
    Version:
      type: object
      required: [id, project_id, dataset, type, version, file_path, file_type, created_at, message]
      properties:
        id: {type: integer}
        project_id: {type: integer}
        folder_id: {type: integer, format: int64}
        dataset: {type: string}
        type: {type: string}
        version: {type: integer}
        file_path: {type: string}
        file_type: {type: string}
        created_at: {type: string}
        created_by: {type: integer, format: int64}
        message: {type: string}
        status: {type: string}
        operation: {type: string}
        parent_version: {type: integer}
        rollback_source_version_id: {type: integer}
        datasource_type: {type: string}
        datasource_mode: {type: string}
        datasource_url: {type: string}
        datasource_meta: {}
        connection_id: {type: integer}
        schema_name: {type: string}
        table_name: {type: string}
        geometry_column: {type: string}
    DatasetSummary:
      type: object
      required: [name, type, latest_version, latest_version_id, file_path, file_type, visibility, permissions, archived]
      properties:
        name: {type: string}
        title: {type: string}
        type: {type: string}
        latest_version: {type: integer}
        latest_version_id: {type: integer}
        file_path: {type: string}
        file_type: {type: string}
        datasource_type: {type: string}
        datasource_mode: {type: string}
        datasource_url: {type: string}
        datasource_meta: {}
        datasource_label: {type: string}
        owner: {type: string}
        created_by: {type: integer, format: int64}
        created_at: {type: string}
        visibility: {type: string}
        permissions: {type: array, items: {type: string}}
        archived: {type: boolean}
        archived_at: {type: string}
        folder_id: {type: integer, format: int64}
    Discovery:
      type: object
      required: [service, resource_type, items]
      properties:
        service: {type: string}
        resource_type: {type: string}
        items:
          type: array
          items:
            type: object
            required: [id, title, description, url, category]
            properties: {id: {type: string}, title: {type: string}, description: {type: string}, url: {type: string}, thumbnail: {type: string}, category: {type: string}}

    PostGISSidecar:
      type: object
      properties:
        postgis_import: {type: boolean}
        postgis_destination: {type: string}
        postgis_connection_id: {type: integer}
        postgis_database_id: {type: integer}
        postgis_new_database_name: {type: string}
        postgis_schema: {type: string}
        postgis_table_name: {type: string}
        postgis_mode: {type: string}
        postgis_owner_mode: {type: string}
        postgis_owner_username: {type: string}
        postgis_owner_password: {type: string, format: password}
        postgis_pg_service_name: {type: string}
        postgis_host: {type: string}
        postgis_port: {}
        postgis_dbname: {type: string}
        postgis_user: {type: string}
        postgis_password: {type: string, format: password}
        postgis_sslmode: {type: string}
    GeoServerSidecar:
      type: object
      properties:
        publish_to_geoserver: {type: boolean}
        geoserver_connection_id: {type: integer}
        geoserver_workspace: {type: string}
        geoserver_store_name: {type: string}
        geoserver_layer_name: {type: string}
        geoserver_style: {type: string}
        geoserver_publish_immediate: {type: boolean}
    Upload:
      allOf:
        - {$ref: "#/components/schemas/PostGISSidecar"}
        - {$ref: "#/components/schemas/GeoServerSidecar"}
        - type: object
          required: [file]
          properties:
            file: {type: string, format: binary, description: "Effective accepted extensions: .gpkg, .geojson, .json, .zip, .tif, .tiff. ZIP uploads must contain .shp, .dbf, and .shx. Although the initial gate lists .kml, .qgs, and .qgz, later type detection rejects them."}
            folder_id: {type: integer, format: int64}
    ImportURL:
      allOf:
        - {$ref: "#/components/schemas/PostGISSidecar"}
        - {$ref: "#/components/schemas/GeoServerSidecar"}
        - type: object
          required: [dataset_name, url]
          properties: {dataset_name: {type: string}, url: {type: string, format: uri}, version_label: {type: string}, project_id: {type: integer}, folder_id: {type: integer, format: int64}}
    ImportS3:
      allOf:
        - {$ref: "#/components/schemas/PostGISSidecar"}
        - {$ref: "#/components/schemas/GeoServerSidecar"}
        - type: object
          required: [dataset_name, bucket, key]
          properties:
            dataset_name: {type: string}
            bucket: {type: string}
            key: {type: string}
            endpoint: {type: string}
            region: {type: string}
            access_key: {type: string}
            secret_key: {type: string, format: password}
            path_style: {type: boolean}
            mode: {type: string, enum: [import, remote]}
            version_label: {type: string}
            project_id: {type: integer}
            folder_id: {type: integer, format: int64}
    Clone:
      type: object
      properties:
        name: {type: string, description: Defaults to "<source> (Copy)" when empty or omitted}
        clone_metadata: {type: boolean}
        clone_versions: {type: boolean}
        clone_publish_config: {type: boolean, description: Accepted but currently ignored}
    UserMetadataInput:
      type: object
      properties: {title: {type: string}, description: {type: string}, tags: {type: string}, citation: {type: string}, attribution: {type: string}, keywords: {type: string}, notes: {type: string}}
    UserMetadata:
      allOf:
        - {$ref: "#/components/schemas/UserMetadataInput"}
        - type: object
          properties:
            dataset_id: {type: integer, format: int64}
            dataset_name: {type: string}
            updated_at: {type: string}
            archived: {type: boolean}
            archived_at: {type: string}
            folder_id: {type: integer, format: int64}
            folder_name: {type: string}
    Publish:
      type: object
      required: [version_id]
      properties: {version_id: {type: integer}, dataset: {type: string}}
    PublishCapability:
      type: object
      required: [available]
      properties: {available: {type: boolean}, target: {type: string}, reason: {type: string}}
    Filter:
      type: object
      required: [where]
      properties: {layer: {type: string}, where: {type: string}}
    Dissolve:
      type: object
      required: [mode]
      properties:
        layer: {type: string}
        mode: {type: string, enum: [all, field]}
        field: {type: string}
        aggregations: {type: array, items: {type: string}}
    Clip:
      type: object
      required: [mode]
      properties:
        mode: {type: string, enum: [draw, bbox, dataset]}
        geometry: {}
        bbox: {type: array, items: {type: number}}
        clip_dataset: {type: string}
        clip_layer: {type: string}
    FilterClause:
      type: object
      required: [column, op]
      properties: {column: {type: string}, op: {type: string}, value: {}}
    ColumnSchema:
      type: object
      required: [name, data_type, udt_name, nullable, editable, is_geometry, is_pk, show_in_table]
      properties: {name: {type: string}, data_type: {type: string}, udt_name: {type: string}, nullable: {type: boolean}, editable: {type: boolean}, is_geometry: {type: boolean}, is_pk: {type: boolean}, show_in_table: {type: boolean}}
    EditSchema:
      type: object
      required: [columns, identity_column, identity_kind, editable]
      properties:
        schema: {type: string}
        table: {type: string}
        layer: {type: string}
        file_path: {type: string}
        columns: {type: array, items: {$ref: "#/components/schemas/ColumnSchema"}}
        pk_columns: {type: array, items: {type: string}}
        identity_column: {type: string}
        identity_kind: {type: string}
        editable: {type: boolean}
    Rows:
      type: object
      required: [columns, rows, total, limit, offset, identity_column, identity_kind]
      properties:
        schema: {type: string}
        table: {type: string}
        layer: {type: string}
        columns: {type: array, items: {$ref: "#/components/schemas/ColumnSchema"}}
        column_names: {type: array, items: {type: string}}
        rows: {type: array, items: {type: object, additionalProperties: true}}
        total: {type: integer}
        limit: {type: integer}
        offset: {type: integer}
        identity_column: {type: string}
        identity_kind: {type: string}
    EditUpdate:
      type: object
      required: [pk_column, pk_value, updates]
      properties: {pk_column: {type: string}, pk_value: {}, updates: {type: object, additionalProperties: true}}
    EditDelete:
      type: object
      required: [pk_column, pk_value]
      properties: {pk_column: {type: string}, pk_value: {}}
    EditInsert:
      type: object
      required: [row]
      properties: {row: {type: object, additionalProperties: true}}

    PgConnectionBase:
      type: object
      properties:
        name: {type: string}
        host: {type: string}
        port: {type: integer, default: 5432}
        database_name: {type: string}
        username: {type: string}
        password: {type: string, format: password}
        sslmode: {type: string, enum: [disable, require, verify-ca, verify-full]}
    PgConnectionCreate:
      allOf:
        - {$ref: "#/components/schemas/PgConnectionBase"}
        - {type: object, required: [name, host, database_name, username, password]}
    PgConnectionUpdate:
      allOf:
        - {$ref: "#/components/schemas/PgConnectionBase"}
        - {type: object, required: [name, host, database_name, username]}
    TestConnection:
      allOf:
        - {$ref: "#/components/schemas/PgConnectionBase"}
        - type: object
          properties: {id: {type: integer}}
    PgConnectionPublic:
      type: object
      required: [id, name, host, port, database_name, username, sslmode]
      properties: {id: {type: integer}, name: {type: string}, host: {type: string}, port: {type: integer}, database_name: {type: string}, username: {type: string}, sslmode: {type: string}, created_at: {type: string}}
    SpatialTable:
      type: object
      required: [f_table_schema, f_table_name, f_geometry_column, srid, type]
      properties: {f_table_schema: {type: string}, f_table_name: {type: string}, f_geometry_column: {type: string}, srid: {type: integer}, type: {type: string}}
    TableTarget:
      type: object
      required: [connection_id, table]
      properties: {connection_id: {type: integer}, schema: {type: string, default: public}, table: {type: string}}
    TableUpdate:
      allOf:
        - {$ref: "#/components/schemas/TableTarget"}
        - {$ref: "#/components/schemas/EditUpdate"}
    TableDelete:
      allOf:
        - {$ref: "#/components/schemas/TableTarget"}
        - {$ref: "#/components/schemas/EditDelete"}
    TableInsert:
      allOf:
        - {$ref: "#/components/schemas/TableTarget"}
        - {$ref: "#/components/schemas/EditInsert"}
    RegisterPostGIS:
      type: object
      required: [name, connection_id, table_name]
      properties: {name: {type: string}, connection_id: {type: integer}, schema_name: {type: string}, table_name: {type: string}, geometry_column: {type: string}, project_id: {type: integer}, folder_id: {type: integer, format: int64}}
    DatabaseService:
      type: object
      required: [name, host, port, dbname, user]
      properties: {name: {type: string}, host: {type: string}, port: {type: string}, dbname: {type: string}, user: {type: string}, sslmode: {type: string}}
    AdoptPostGIS:
      type: object
      required: [database_id]
      properties: {connection_id: {type: integer}, database_id: {type: integer}, name: {type: string}}

    QuickImport:
      allOf:
        - {$ref: "#/components/schemas/PostGISSidecar"}
        - type: object
          required: [source_type]
          properties:
            source_type: {type: string, enum: [url, s3, esri, geoserver, postgis]}
            url: {type: string}
            s3_bucket: {type: string}
            s3_key: {type: string}
            s3_endpoint: {type: string}
            s3_region: {type: string}
            s3_access_key: {type: string}
            s3_secret_key: {type: string, format: password}
            s3_path_style: {type: boolean}
            esri_service_url: {type: string}
            esri_username: {type: string}
            esri_password: {type: string, format: password}
            esri_token: {type: string}
            esri_layer_ids: {type: array, items: {type: integer}}
            esri_prefix: {type: string}
            geoserver_connection_id: {type: integer}
            geoserver_workspace: {type: string}
            geoserver_layer: {type: string}
            source_database_id: {type: integer}
            source_schema: {type: string}
            source_tables: {type: array, items: {type: string}}
            schema: {type: string}
            table_name: {type: string}
            mode: {type: string}
    QuickImportUpload:
      allOf:
        - {$ref: "#/components/schemas/PostGISSidecar"}
        - type: object
          required: [file]
          properties: {file: {type: string, format: binary}, source_type: {type: string, const: upload}, schema: {type: string}, table_name: {type: string}, mode: {type: string}}
    ESRIBrowse:
      type: object
      required: [service_url]
      properties: {service_url: {type: string}, username: {type: string}, password: {type: string, format: password}}
    ESRILayer:
      type: object
      required: [id, name, type, geometry_type, supports_query, max_record_count, layer_url]
      properties: {id: {type: integer}, name: {type: string}, type: {type: string}, geometry_type: {type: string}, supports_query: {type: boolean}, max_record_count: {type: integer}, layer_url: {type: string}}
    ESRIBrowseResponse:
      type: object
      properties:
        service:
          type: object
          properties: {service_url: {type: string}, service_type: {type: string}, layers: {type: array, items: {$ref: "#/components/schemas/ESRILayer"}}}
        token: {type: string}
    ESRIImport:
      type: object
      required: [service_url, layer_ids]
      properties:
        service_url: {type: string}
        username: {type: string}
        password: {type: string, format: password}
        token: {type: string}
        layer_ids: {type: array, items: {type: integer}}
        postgis_destination: {type: string, enum: [existing, new]}
        postgis_database_id: {type: integer}
        postgis_connection_id: {type: integer}
        postgis_new_database_name: {type: string}
        schema: {type: string}
        dataset_prefix: {type: string}
        run_as_background_job: {type: boolean}
        folder_id: {type: integer, format: int64}

    GeoServerConnectionBase:
      type: object
      properties: {name: {type: string}, url: {type: string}, username: {type: string}, password: {type: string, format: password}, default_workspace: {type: string}, enabled: {type: boolean}, verify_ssl: {type: boolean}}
    GeoServerConnectionCreate:
      allOf:
        - {$ref: "#/components/schemas/GeoServerConnectionBase"}
        - {type: object, required: [name, url, username, password]}
    GeoServerConnectionUpdate:
      allOf:
        - {$ref: "#/components/schemas/GeoServerConnectionBase"}
        - {type: object, required: [name, url, username]}
    GeoServerWorkspace:
      type: object
      required: [connection_id, name]
      properties: {connection_id: {type: integer}, name: {type: string}, recurse: {type: boolean}}
    GeoServerCatalogImport:
      type: object
      required: [connection_id, workspace, layer_name]
      properties: {connection_id: {type: integer}, workspace: {type: string}, layer_name: {type: string}, dataset_name: {type: string}, folder_id: {type: integer, format: int64}}
    GeoServerStyleJSON:
      type: object
      required: [connection_id, style_name, sld]
      properties: {connection_id: {type: integer}, style_name: {type: string}, sld: {type: string}}
    GeoServerStyleMultipart:
      type: object
      required: [connection_id, style_name, file]
      properties: {connection_id: {type: integer}, style_name: {type: string}, file: {type: string, format: binary}}
    GeoServerStyleAssign:
      type: object
      required: [connection_id, workspace, layer_name, style_name]
      properties: {connection_id: {type: integer}, workspace: {type: string}, layer_name: {type: string}, style_name: {type: string}}
    GeoServerPublish:
      allOf:
        - {$ref: "#/components/schemas/GeoServerSidecar"}
        - type: object
          required: [dataset_name, version, publish_to_geoserver, geoserver_connection_id, geoserver_workspace]
          properties: {dataset_name: {type: string}, version: {type: integer}}
    FeatureCollection:
      type: object
      required: [type, features]
      properties:
        type: {type: string, const: FeatureCollection}
        features: {type: array, items: {type: object, additionalProperties: true}}
      additionalProperties: true
