openapi: 3.1.0
info:
  title: lt-database-api
  version: 0.1.1
  description: 'Signed JSON API in front of PostgreSQL, MySQL and MongoDB.

    Every POST body is a signed envelope; the operation payload is a JSON string inside
    it.

    The signing rules are in docs/SECURITY.md.


    A required write-behind flush that halts blocks dependent operations with 503
    write_buffer_halted. See API.md and WRITE-BEHIND.md for errors and delivery guarantees.

    '
servers:
- url: https://db.example.com
paths:
  /v1/health:
    get:
      summary: Liveness and server time (no authentication)
      responses:
        '200':
          description: Server is up
          content:
            application/json:
              schema:
                type: object
                properties:
                  status:
                    type: string
                    enum:
                    - ok
                  time:
                    type: integer
                    description: Unix time in seconds
    post:
      summary: Authenticated health check (provider and database reachability)
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: health
      x-result-payload-schema:
        $ref: '#/components/schemas/HealthResult'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/schema/sync:
    post:
      summary: Apply a declared schema to a namespace
      description: 'Payload: SyncRequest. Result payload: SyncResult. The wire body
        is RequestEnvelope; payload is a JSON string. x-payload-schema and x-result-payload-schema
        describe that decoded string, not the wire body.'
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: schema_sync
      x-payload-schema:
        $ref: '#/components/schemas/SyncRequest'
      x-result-payload-schema:
        $ref: '#/components/schemas/SyncResult'
  /v1/schema/describe:
    post:
      summary: Return the stored schema of a namespace
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: schema_describe
      x-payload-schema:
        $ref: '#/components/schemas/NamespaceRequest'
      x-result-payload-schema:
        $ref: '#/components/schemas/DescribeResult'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/insert:
    post:
      summary: Insert rows
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_insert
      x-payload-schema:
        $ref: '#/components/schemas/InsertRequest'
      x-result-payload-schema:
        oneOf:
        - $ref: '#/components/schemas/InsertResult'
        - $ref: '#/components/schemas/QueuedAck'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/find:
    post:
      summary: Query rows
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_find
      x-payload-schema:
        $ref: '#/components/schemas/FindRequest'
      x-result-payload-schema:
        $ref: '#/components/schemas/FindResult'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/update:
    post:
      summary: Change rows matching a filter
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_update
      x-payload-schema:
        $ref: '#/components/schemas/UpdateRequest'
      x-result-payload-schema:
        oneOf:
        - $ref: '#/components/schemas/AffectedResult'
        - $ref: '#/components/schemas/QueuedAck'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/upsert:
    post:
      summary: Insert a row or update the existing one with the same key
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_upsert
      x-payload-schema:
        $ref: '#/components/schemas/UpsertRequest'
      x-result-payload-schema:
        oneOf:
        - $ref: '#/components/schemas/AffectedResult'
        - $ref: '#/components/schemas/QueuedAck'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/delete:
    post:
      summary: Delete rows matching a filter
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_delete
      x-payload-schema:
        $ref: '#/components/schemas/DeleteRequest'
      x-result-payload-schema:
        oneOf:
        - $ref: '#/components/schemas/AffectedResult'
        - $ref: '#/components/schemas/QueuedAck'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
  /v1/data/flush:
    post:
      summary: Apply queued writes of a namespace now (write-behind mode)
      description: 'Payload: {namespace}. Result: {flushed, deadLettered, remaining,
        halted, enabled}. The wire body is RequestEnvelope; payload is a JSON string.
        x-payload-schema and x-result-payload-schema describe that decoded string,
        not the wire body.'
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_flush
      x-payload-schema:
        $ref: '#/components/schemas/NamespaceRequest'
      x-result-payload-schema:
        $ref: '#/components/schemas/FlushResult'
  /v1/data/count:
    post:
      summary: Count rows
      requestBody:
        $ref: '#/components/requestBodies/Envelope'
      responses:
        '200':
          $ref: '#/components/responses/Standard'
        default:
          $ref: '#/components/responses/Standard'
      operationId: data_count
      x-payload-schema:
        $ref: '#/components/schemas/CountRequest'
      x-result-payload-schema:
        $ref: '#/components/schemas/CountResult'
      description: The wire body is RequestEnvelope; payload is a JSON string. x-payload-schema
        and x-result-payload-schema describe that decoded string, not the wire body.
components:
  requestBodies:
    Envelope:
      required: true
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/RequestEnvelope'
  responses:
    Standard:
      description: A response envelope. Success is 200; failures use the status of
        the error.
      content:
        application/json:
          schema:
            $ref: '#/components/schemas/ResponseEnvelope'
  schemas:
    RequestEnvelope:
      type: object
      required:
      - v
      - kid
      - ts
      - nonce
      - payload
      - sig
      properties:
        v:
          type: integer
          enum:
          - 1
        kid:
          type: string
          description: Key id
        ts:
          type: integer
          description: Unix time in seconds
        nonce:
          type: string
          minLength: 1
          maxLength: 64
          description: Unique per request
        payload:
          type: string
          description: The operation's JSON
          as a string; signed byte for byte: null
        sig:
          type: string
          description: base64url(HMAC-SHA256(secret, 'v1\n' + path + '\n' + kid +
            '\n' + ts + '\n' + nonce + '\n' + payload))
    ResponseEnvelope:
      type: object
      required:
      - v
      - ok
      - status
      - payload
      properties:
        v:
          type: integer
        ok:
          type: boolean
        status:
          type: integer
        payload:
          type: string
          description: JSON string with the result or an error object
        sig:
          type: string
          description: base64url(HMAC-SHA256(secret, 'v1resp\n' + requestNonce + '\n'
            + status + '\n' + payload)); absent on unauthenticated errors
        code:
          type: string
    ColumnType:
      type: string
      enum:
      - int32
      - int64
      - decimal
      - float64
      - bool
      - string
      - text
      - json
      - datetime
      - date
      - uuid
      - bytes
    Filter:
      description: A condition or an and/or/not group.
      oneOf:
      - type: object
        required:
        - field
        - op
        properties:
          field:
            type: string
          op:
            type: string
            enum:
            - eq
            - ne
            - gt
            - gte
            - lt
            - lte
            - in
            - nin
            - isNull
            - startsWith
            - contains
          value: {}
      - type: object
        properties:
          and:
            type: array
            items:
              $ref: '#/components/schemas/Filter'
          or:
            type: array
            items:
              $ref: '#/components/schemas/Filter'
          not:
            $ref: '#/components/schemas/Filter'
        minProperties: 1
        maxProperties: 1
    Column:
      type: object
      required:
      - name
      - type
      properties:
        name:
          type: string
        type:
          $ref: '#/components/schemas/ColumnType'
        length:
          type: integer
        precision:
          type: integer
        scale:
          type: integer
        nullable:
          type: boolean
          default: false
        default: {}
        unique:
          type: boolean
          default: false
        autoIncrement:
          type: boolean
          default: false
        renamedFrom:
          type: string
    Table:
      type: object
      required:
      - name
      - columns
      properties:
        name:
          type: string
        primaryKey:
          type: array
          items:
            type: string
        columns:
          type: array
          items:
            $ref: '#/components/schemas/Column'
        indexes:
          type: array
          items:
            type: object
            required:
            - name
            - columns
            properties:
              name:
                type: string
              columns:
                type: array
                items:
                  type: string
              unique:
                type: boolean
        renamedFrom:
          type: string
    SyncRequest:
      type: object
      required:
      - namespace
      - schema
      properties:
        namespace:
          type: string
        dryRun:
          type: boolean
        schema:
          type: object
          required:
          - tables
          properties:
            tables:
              type: array
              items:
                $ref: '#/components/schemas/Table'
    SyncResult:
      type: object
      properties:
        namespace:
          type: string
        hash:
          type: string
        changed:
          type: boolean
        dryRun:
          type: boolean
        status:
          type: string
          enum:
          - unchanged
          - planned
          - applied
          - denied
        plan:
          type: array
          items:
            type: object
            properties:
              kind:
                type: string
              table:
                type: string
              column:
                type: string
              newName:
                type: string
              index:
                type: string
              note:
                type: string
              destructive:
                type: boolean
    NamespaceRequest:
      type: object
      properties:
        namespace: &id001
          type: string
      required:
      - namespace
    InsertRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        durability: &id003
          type: string
          enum:
          - immediate
          description: Bypass buffering after draining older namespace writes.
        idempotencyKey: *id001
        rows:
          type: array
          minItems: 1
          items: &id004
            type: object
            additionalProperties: true
      required:
      - namespace
      - table
      - rows
    FindRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        filter:
          $ref: '#/components/schemas/Filter'
        fields:
          type: array
          items: *id001
        sort:
          type: array
          items:
            type: object
            properties:
              field: *id001
              dir:
                type: string
                enum:
                - asc
                - desc
            required:
            - field
        limit: &id002
          type: integer
        offset: *id002
      required:
      - namespace
      - table
    UpdateRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        durability: *id003
        idempotencyKey: *id001
        filter:
          $ref: '#/components/schemas/Filter'
        set: *id004
        inc: *id004
        all: &id005
          type: boolean
      required:
      - namespace
      - table
    UpsertRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        durability: *id003
        idempotencyKey: *id001
        row: *id004
        keys:
          type: array
          minItems: 1
          items: *id001
      required:
      - namespace
      - table
      - row
      - keys
    DeleteRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        durability: *id003
        idempotencyKey: *id001
        filter:
          $ref: '#/components/schemas/Filter'
        all: *id005
      required:
      - namespace
      - table
    CountRequest:
      type: object
      properties:
        namespace: *id001
        table: *id001
        filter:
          $ref: '#/components/schemas/Filter'
      required:
      - namespace
      - table
    InsertResult:
      type: object
      properties:
        inserted: *id002
        ids:
          type: array
          items: {}
    FindResult:
      type: object
      properties:
        rows:
          type: array
          items: *id004
        hasMore: *id005
    AffectedResult:
      type: object
      properties:
        affected: *id002
    CountResult:
      type: object
      properties:
        count: *id002
    QueuedAck:
      type: object
      properties:
        queued:
          const: true
        id: *id001
        pending: *id002
      required:
      - queued
      - id
      - pending
    FlushResult:
      type: object
      properties:
        flushed: *id002
        deadLettered: *id002
        remaining: *id002
        halted: *id005
        enabled: *id005
    DescribeResult:
      type: object
      properties:
        namespace: *id001
        hash:
          type:
          - string
          - 'null'
        schema:
          oneOf:
          - type: object
            properties:
              tables:
                type: array
                items:
                  $ref: '#/components/schemas/Table'
          - type: 'null'
        updatedAt:
          type:
          - string
          - 'null'
          format: date-time
    HealthResult:
      type: object
      properties:
        status: *id001
        provider: *id001
        database: *id005
        writeBuffer:
          type: object
          properties:
            enabled: *id005
            pending: *id002
            oldestAgeSeconds: *id002
            halted: *id005
