$schema: "http://json-schema.org/draft-07/schema#"
title: flusso Index Schema
type: object
required: [version, table, fields]
additionalProperties: false
properties:
  version:
    type: integer
    enum: [1]
  table:
    $ref: "#/definitions/pg_identifier"
  schema:
    $ref: "#/definitions/pg_identifier"
    default: public
  primary_key:
    $ref: "#/definitions/pg_identifier"
  doc_id:
    $ref: "#/definitions/pg_identifier"
  soft_delete:
    oneOf:
      - type: object
        required: [field]
        additionalProperties: false
        properties:
          field:
            $ref: "#/definitions/field_name"
          when:
            $ref: "#/definitions/filters"
      - type: object
        required: [column]
        additionalProperties: false
        properties:
          column:
            $ref: "#/definitions/pg_identifier"
          when:
            $ref: "#/definitions/filters"
  filters:
    $ref: "#/definitions/filters"
    description: >-
      Root filters: only root rows matching every filter become documents. A
      row that stops matching emits a tombstone, like soft_delete.
  fields:
    type: array
    items:
      $ref: "#/definitions/field"

definitions:
  pg_identifier:
    type: string
    pattern: "^[a-z_][a-z0-9_]*$"
    maxLength: 63

  field_name:
    type: string
    pattern: "^[a-zA-Z_][a-zA-Z0-9_]*$"
    maxLength: 63

  # A field is written **type-first**: exactly one type key (its value is the
  # document key) plus that type's siblings.
  #
  #   - keyword: email          # scalar leaf
  #     required: true
  #   - geo: location           # geo_point from two columns
  #     lat: latitude
  #     lon: longitude
  #     required: false
  #   - belongs_to: created_by  # join: MY column points at the related row
  #     table: users            # (`column` defaults to the field name)
  #     primary_key: id
  #     fields: [ … ]
  #   - has_many: orders        # join: THEIR foreign_key points back at me
  #     table: orders
  #     foreign_key: user_id
  #     primary_key: id
  #     fields: [ … ]
  #   - count: orderCount       # aggregate (op is the key)
  #     table: orders
  #     foreign_key: user_id
  #
  # This schema lists every legal key for editor assist; the loader enforces the
  # rules JSON Schema can't state here: exactly one type key per field, and the
  # per-type sibling set (e.g. `count` takes no `column`; `sum`/`min`/`max`
  # require `column` + `value_type`; a `geo` needs `lat`+`lon` or a single
  # `column`; a join takes exactly the key its verb implies — `column` for
  # `belongs_to`, `foreign_key` for `has_one`/`has_many`, `through` for
  # `many_to_many`).
  field:
    type: object
    additionalProperties: false
    properties:
      # ── scalar leaf types (value = document key) ──────────────────────────
      text: { $ref: "#/definitions/field_name" }
      identifier: { $ref: "#/definitions/field_name" }
      keyword: { $ref: "#/definitions/field_name" }
      enum: { $ref: "#/definitions/field_name" }
      uuid: { $ref: "#/definitions/field_name" }
      boolean: { $ref: "#/definitions/field_name" }
      short: { $ref: "#/definitions/field_name" }
      integer: { $ref: "#/definitions/field_name" }
      long: { $ref: "#/definitions/field_name" }
      float: { $ref: "#/definitions/field_name" }
      double: { $ref: "#/definitions/field_name" }
      decimal: { $ref: "#/definitions/field_name" }
      date: { $ref: "#/definitions/field_name" }
      timestamp: { $ref: "#/definitions/field_name" }
      binary: { $ref: "#/definitions/field_name" }
      json: { $ref: "#/definitions/field_name" }
      # ── custom scalar, geo, object ────────────────────────────────────────
      custom: { $ref: "#/definitions/field_name" }
      geo: { $ref: "#/definitions/field_name" }
      object: { $ref: "#/definitions/field_name" }
      # ── joins (the relationship verb is the key) ──────────────────────────
      belongs_to: { $ref: "#/definitions/field_name" }
      has_one: { $ref: "#/definitions/field_name" }
      has_many: { $ref: "#/definitions/field_name" }
      many_to_many: { $ref: "#/definitions/field_name" }
      # ── aggregates (op is the key) ────────────────────────────────────────
      count: { $ref: "#/definitions/field_name" }
      sum: { $ref: "#/definitions/field_name" }
      avg: { $ref: "#/definitions/field_name" }
      min: { $ref: "#/definitions/field_name" }
      max: { $ref: "#/definitions/field_name" }
      ids: { $ref: "#/definitions/field_name" }
      # ── constant ──────────────────────────────────────────────────────────
      constant: { $ref: "#/definitions/field_name" }

      # ── siblings (which apply depends on the type key) ────────────────────
      # Leaf / scalar (`column` also keys a `belongs_to` join — this table's
      # column pointing at the related row, defaulting to the field name):
      required:
        type: boolean
        description: Whether the field may be null. Mandatory on a scalar leaf.
      column:
        $ref: "#/definitions/pg_identifier"
      options:
        type: object
        additionalProperties: true
        description: >-
          Extra OpenSearch mapping properties merged beside the derived type
          (e.g. analyzer, format, scaling_factor).
      transforms:
        $ref: "#/definitions/transforms"
      default: {}
      # custom:
      postgres:
        type: array
        items:
          type: string
      opensearch:
        type: string
      # geo (two-column form):
      lat:
        $ref: "#/definitions/pg_identifier"
      lon:
        $ref: "#/definitions/pg_identifier"
      # object / join (nested projection):
      fields:
        type: array
        items:
          $ref: "#/definitions/field"
      # join / aggregate (related table). `foreign_key` is the related table's
      # column pointing back at this row (`has_one`/`has_many`/aggregates):
      table:
        $ref: "#/definitions/pg_identifier"
      primary_key:
        $ref: "#/definitions/pg_identifier"
      foreign_key:
        $ref: "#/definitions/pg_identifier"
      through:
        $ref: "#/definitions/through"
      filters:
        $ref: "#/definitions/filters"
      order_by:
        $ref: "#/definitions/order_by"
      limit:
        type: integer
        minimum: 1
      # aggregate (sum/min/max result type):
      value_type:
        $ref: "#/definitions/flusso_type"
      # aggregate (ids element type — the scalar type of each collected key):
      element_type:
        $ref: "#/definitions/flusso_type"
      # constant:
      value: {}

  flusso_type:
    description: >-
      A scalar field's declared type — used as a field's type key, and as the
      `value_type` of a `sum`/`min`/`max` aggregate. Each bridges a Postgres
      column type and an OpenSearch mapping type.
    enum:
      [
        text,
        identifier,
        keyword,
        enum,
        uuid,
        boolean,
        short,
        integer,
        long,
        float,
        double,
        decimal,
        date,
        timestamp,
        binary,
        json,
      ]

  transforms:
    type: array
    items:
      type: string
      enum: [lowercase, trim]

  filter:
    oneOf:
      - type: object
        required: [raw]
        additionalProperties: false
        properties:
          raw:
            type: string
            minLength: 1
      - type: object
        required: [column, op]
        additionalProperties: false
        properties:
          column:
            $ref: "#/definitions/pg_identifier"
          op:
            type: string
            enum: [is_null, is_not_null]
      - type: object
        required: [column, op]
        additionalProperties: false
        properties:
          column:
            $ref: "#/definitions/pg_identifier"
          op:
            type: string
            enum: [eq, neq, lt, lte, gt, gte, in, not_in, like, ilike, between]
          value: {}

  filters:
    type: array
    items:
      $ref: "#/definitions/filter"

  order_by:
    type: array
    items:
      type: object
      required: [column]
      additionalProperties: false
      properties:
        column:
          $ref: "#/definitions/pg_identifier"
        direction:
          type: string
          enum: [asc, desc]

  through:
    type: object
    required: [table, left_key, right_key]
    additionalProperties: false
    properties:
      table:
        $ref: "#/definitions/pg_identifier"
      left_key:
        $ref: "#/definitions/pg_identifier"
      right_key:
        $ref: "#/definitions/pg_identifier"
