# A query giving a map layer

A query can return its records as a map layer: a
`FeatureCollection<G, P>` ([Interop](geospatial-interop.md)), where `G`
is the geometry type and `P` the type of each feature's properties. A
`GET` route can then send the layer a mapping library fetches, as plain
RFC 7946 GeoJSON ([Output and status](output-and-status.md)).
Here are a bakery map's layer, and the bakeries within reach of a door:

```jadpo
use baking/entities { Bakery }
use baking/types { BakeryName }

/// What a bakery's pin on the map says: its identity goes in its
/// properties.
type BakeryPin = Object {
    id: Bakery.id
    name: BakeryName
}
```

```jadpo
use baking/entities { Bakery }
use baking/types { BakeryPin }

/// Every bakery, as the map's layer.
query bakery_map() -> FeatureCollection<Point, BakeryPin> {
    from: Bakery
    geometry: Bakery.spot
    order: [Bakery.name asc]
    limit: 500
}
```

```jadpo
use baking/entities { Bakery }
use baking/types { BakeryPin, SearchReach }

/// The bakeries within reach of a door, nearest first, as a layer.
query bakeries_near_map(
    doorstep: Point,
    max_trip: SearchReach
) -> FeatureCollection<Point, BakeryPin> {
    from: Bakery
    geometry: Bakery.spot
    where: geo.within(Bakery.spot, doorstep, distance: max_trip)
    order: [geo.distance(Bakery.spot, doorstep) asc]
    limit: 20
}
```

```jadpo
use baking/types { BakeryPin }
use baking/queries { bakery_map }

/// Bakeries, for the map.
routes Bakeries {
    prefix: /bakeries

    /// Every bakery, as GeoJSON.
    GET / {
        authentication: RouteAuthentication.none
        output: FeatureCollection<Point, BakeryPin>
        run: bakery_map()
    }
}
```

```jadpo
use baking/entities { Bakery }
use baking/types { BakeryPin }
use baking/actions { open_bakery }
use baking/queries { bakeries_near_map, bakery_map }

test "the map fetches the bakeries as GeoJSON" {
    var soho: Bakery = open_bakery(
        "Soho",
        Point {
            latitude: 51.5136
            longitude: -0.1365
            altitude: none
        }
    )
    var bermondsey: Bakery = open_bakery(
        "Bermondsey",
        Point {
            latitude: 51.4979
            longitude: -0.0637
            altitude: none
        }
    )
    var response: Response = request {
        method: HttpMethod.get
        path: "/bakeries"
    }
    assert response.status == 200
    assert response.output == bakery_map()
    assert response.output == FeatureCollection {
        features: [
            Feature {
                geometry: bermondsey.spot
                properties: BakeryPin {
                    id: bermondsey.id
                    name: bermondsey.name
                }
            },
            Feature {
                geometry: soho.spot
                properties: BakeryPin {
                    id: soho.id
                    name: soho.name
                }
            }
        ]
    }
    var near_soho: FeatureCollection<Point, BakeryPin> = bakeries_near_map(soho.spot, 2km)
    assert list.map(near_soho.features, pin -> pin.properties.name) == ["Soho"]
}
```

`GET /bakeries` answers `{"type":"FeatureCollection","features":[...]}`,
each feature `{"type":"Feature","geometry":{"type":"Point","coordinates":[-0.0637,51.4979]},"properties":{"id":"...","name":"Bermondsey"}}`.
`jadpo sql bakery_map` prints its one statement, a `SELECT` of the
properties' columns and the geometry's.

The result type names the properties type, as `List<Nearest<T>>` names its
records. `geometry:` names the stored field that is each feature's
geometry, a `G` every record has. It has to be named because an entity may
hold several geometry fields, and the result type can't say which. `P`
must be an `Object` type, as a feature's properties always are
([Interop](geospatial-interop.md)). Its fields are filled from the
record's fields of the same names, as any result type's are.

The query picks fields and computes nothing, so it is a bounded list like
any other: `order:` and `limit:`, with `geo.` conditions and an order by
`geo.distance` if it needs them ([Queries](geospatial-queries.md)).

- **`geometry:`** names a field of the entity read, of exactly the layer's
  own geometry type, and never `none`, since a feature always has a
  geometry. So a `Point?` field, a `Polygon` field in a layer of `Point`s,
  or any field in a layer of `Geometry` (which would be a value built from
  it) is `query.geometry_field`. A field the entity doesn't have is
  `query.unknown_field`, as in a condition.
- **One bounded list.** `order:` and `limit:` are required. `page:` is
  refused, since a `FeatureCollection` has no cursor, and so are
  `missing:`, `include:`, `group:` and `aggregate:`, since a layer's
  properties are each record's own fields, one feature to a record.
- **Its properties.** Each field of `P` must be the record's field of that
  name, or the query is `query.result_field`. A feature's identity goes in
  its properties, so `P` picks `id` where the map needs it.
- **Field policy.** A layer is sent as it is, so it is held to what an
  `Output` is held to
  ([Field restrictions](field-restrictions.md)). Every caller who
  may read the records must be allowed to read each property, the
  geometry, and every field the conditions and order test, or the query is
  `policy.output_field_unproved`. This is proved where the query is
  declared, so an action returning the layer carries nothing more.
- **Only what a `GET` can ask for.** A `GET`'s input is its query string
  ([One input](one-input.md)), which can't hold a `BoundingBox`.
  So a layer of only what is in view would still need a body. The teaching
  example `21-veg-box-rounds` sends its whole drop point layer with
  `GET /drop-points`.

A property the record has no field for:

```jadpo
type BakeryName = Text {
    max_length: 80
}

entity Bakery {
    id: Uuid
    name: BakeryName
    spot: Point
}

type BakeryPin = Object {
    bakery_id: Bakery.id
    name: BakeryName
}

query bakery_map() -> FeatureCollection<Point, BakeryPin> {
    from: Bakery
    geometry: Bakery.spot
    order: [Bakery.name asc]
    limit: 500
}
```

```text
error: `BakeryPin.bakery_id` isn't a field of `Bakery`
 --> main.jadpo:16:23
   |
16 | query bakery_map() -> FeatureCollection<Point, BakeryPin> {
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `bakery_id` can't be filled
  reason: A map layer fills its features' properties from `Bakery`'s fields of the same name (`id`, `name` and `spot`); nothing gives `bakery_id`.
  next: Remove `bakery_id` from `BakeryPin`, or name it after a field of `Bakery` (agent)
  rule: query.result_field
  help: https://jadpo.dev/docs/errors/query.result_field

check failed: 1 error, 1 file
```

A property only the server may read:

```jadpo
type BakeryName = Text {
    max_length: 80
}

type PhoneText = Text {
    max_length: 20
}

entity Bakery {
    id: Uuid
    name: BakeryName
    spot: Point
    owner_phone: PhoneText {
        policy: FieldAccess.server_only
    }

    policy: {
        read: [Access.public]
    }
}

type BakeryPin = Object {
    name: BakeryName
    owner_phone: PhoneText
}

query bakery_map() -> FeatureCollection<Point, BakeryPin> {
    from: Bakery
    geometry: Bakery.spot
    order: [Bakery.name asc]
    limit: 500
}
```

```text
error: `bakery_map` sends `Bakery.owner_phone`, which not every caller may read
 --> main.jadpo:27:23
   |
27 | query bakery_map() -> FeatureCollection<Point, BakeryPin> {
   |                       ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ carries `owner_phone`
  reason: Anyone `Bakery`'s `read:` lets in may call `bakery_map`, anyone at all among them, and no client may read it, as it is `FieldAccess.server_only` `owner_phone`. An output has one shape for everyone, so a field some callers may not read can't be in it.
  next: Leave `owner_phone` out of `BakeryPin`, the features' properties (agent)
  rule: policy.output_field_unproved
  help: https://jadpo.dev/docs/errors/policy.output_field_unproved

check failed: 1 error, 1 file
```
