Skip to main content

cratefield_tables_api/
read.rs

1//! Reading a declared table's rows (issue #153).
2//!
3//! Two routes: a page of a table, and one row by its primary key. Both go
4//! through [`crate::access::may_read`] first and carry its answer into
5//! the `WHERE` rather than applying it to rows already fetched — see
6//! [`cratefield_tables::Owned`] for why that distinction is not a
7//! refactoring preference.
8//!
9//! # The database handle
10//!
11//! These take a `&dyn Database`, and the router hands them a
12//! `cratefield_core::TenantConn` — never `ctx.ports.db`. The context is
13//! built once and captured in handler state, so `ports.db` is the same
14//! handle for every request whoever asked; `TenantConn` is the one the
15//! resolution layer resolved for *this* request's tenant. For a venture's
16//! own declared tables that is the difference between serving a row and
17//! serving somebody else's customer their neighbour's data.
18//!
19//! The functions take the trait rather than the `TenantConn` so they can
20//! be exercised against a real database without an HTTP stack —
21//! `TenantConn` has no constructor, which is the point of it. The one
22//! other thing the request needs from that connection — its
23//! [`Tenancy`], the fact a membership rule
24//! reads (#385) — arrives as its own parameter, fed from
25//! `conn.tenancy()` at the routes; the tests here pass the tenancy they
26//! mean to decide under. The routes below are where the tenant-bound
27//! handle is insisted on.
28
29use std::sync::Arc;
30
31use cratefield_core::{
32    Caller, Database, ModuleContext, Problem, ProblemDef, Scope, Tenancy, require_admin,
33};
34use cratefield_tables::{Owned, TableDef};
35use http::{HeaderMap, StatusCode};
36use serde_json::{Value, json};
37
38use crate::access::{Reach, TableApi, may_read};
39
40/// The default page size, and the most a caller may ask for.
41///
42/// One number for both: a cap that differs from the default is a cap
43/// nobody meets until they ask for more, and then meets by surprise.
44pub const PAGE: u64 = 50;
45
46/// No table of that name is declared.
47pub const NO_SUCH_TABLE: ProblemDef = ProblemDef {
48    slug: "no-such-table",
49    status: StatusCode::NOT_FOUND,
50    title: "No such table",
51    description: "This venture declares no table by that name.",
52};
53
54/// The row is not there — or is not the caller's, which is deliberately
55/// the same answer.
56pub const NO_SUCH_ROW: ProblemDef = ProblemDef {
57    slug: "no-such-row",
58    status: StatusCode::NOT_FOUND,
59    title: "No such row",
60    description: "No row of this table has that key.",
61};
62
63/// The deployment cannot identify a caller for a table that needs one.
64pub const NO_VERIFIER: ProblemDef = ProblemDef {
65    slug: "no-verifier",
66    status: StatusCode::INTERNAL_SERVER_ERROR,
67    title: "Cannot identify the caller",
68    description: "This table is not public and the deployment wired no way to verify a caller.",
69};
70
71/// The credential was presented and could not be checked right now.
72pub const VERIFIER_UNAVAILABLE: ProblemDef = ProblemDef {
73    slug: "verifier-unavailable",
74    status: StatusCode::SERVICE_UNAVAILABLE,
75    title: "Cannot check the credential",
76    description: "The service that verifies credentials could not answer. Try again shortly.",
77};
78
79/// The caller ordered by something this table cannot be ordered by.
80pub const BAD_SORT: ProblemDef = ProblemDef {
81    slug: "bad-sort",
82    status: StatusCode::BAD_REQUEST,
83    title: "Not a sort for this table",
84    description: "The `sort` parameter names a column the table does not declare, or one whose nulls the engines order differently.",
85};
86
87/// The caller narrowed by something this table cannot be narrowed by.
88pub const BAD_FILTER: ProblemDef = ProblemDef {
89    slug: "bad-filter",
90    status: StatusCode::BAD_REQUEST,
91    title: "Not a filter for this table",
92    description: "A query parameter names a column the table does not declare, or a value that is not that column's kind.",
93};
94
95/// A credential was presented and did not verify — core's shared 401,
96/// `cratefield_core::SLUGS.unauthenticated`, the same body
97/// [`crate::access::UNAUTHENTICATED`] answers with for no credential at
98/// all: which half failed would be a hint about how to get closer.
99pub const NOT_VERIFIED: ProblemDef = cratefield_core::SLUGS.unauthenticated;
100
101/// What the routes are built over.
102pub struct Tables {
103    /// Every declared table, with its access level and subject column.
104    pub tables: Vec<TableApi>,
105    /// The composed context, for the `Auth` port and the config.
106    pub ctx: Arc<ModuleContext>,
107}
108
109impl Tables {
110    /// The declared table of that name, if the venture declares one.
111    #[must_use]
112    pub fn declared(&self, name: &str) -> Option<&TableApi> {
113        self.tables.iter().find(|api| api.table.name == name)
114    }
115
116    fn find(&self, name: &str) -> Result<&TableApi, Problem> {
117        self.tables
118            .iter()
119            .find(|api| api.table.name == name)
120            .ok_or_else(|| Problem::new(&NO_SUCH_TABLE))
121    }
122}
123
124/// Who is asking, or the refusal to answer at all.
125///
126/// A credential presented and not verified is a refusal even on a public
127/// table, which is the rule the `Auth` port exists to keep (#362): an
128/// expired token reading a public table must not succeed quietly and
129/// leave its holder believing they are signed in.
130pub(crate) async fn who(
131    tables: &Tables,
132    headers: &HeaderMap,
133    api: &TableApi,
134) -> Result<Caller, Problem> {
135    let Some(auth) = tables.ctx.ports.auth.as_ref() else {
136        // A deployment with no verifier can still serve a public table,
137        // because nothing about it depends on who is asking. Anything
138        // else is a composition that should not have booted — a module
139        // serving a non-public table declares `Port::Auth`, and the
140        // harness refuses a module whose required port is not provided.
141        if api.access == cratefield_manifest::Access::PublicRead {
142            return Ok(Caller::Anonymous);
143        }
144        return Err(Problem::new(&NO_VERIFIER));
145    };
146    auth.identify(headers).await.map_err(|err| match err {
147        cratefield_core::AuthError::NotVerified => Problem::new(&NOT_VERIFIED),
148        // A different refusal on purpose: telling a caller to sign in
149        // again while the verifier is down is advice they will follow and
150        // that will not help.
151        cratefield_core::AuthError::Unavailable(_) => Problem::new(&VERIFIER_UNAVAILABLE),
152        // `AuthError` is `#[non_exhaustive]`. A refusal this build cannot
153        // read is a fact about the deployment, not about the caller's
154        // credential, so it answers 503: "try again" asserts nothing
155        // false, where "sign in again" would.
156        other => {
157            tracing::error!(error = %other, "a refusal this build does not understand");
158            Problem::new(&VERIFIER_UNAVAILABLE)
159        }
160    })
161}
162
163/// The scope `may_read` decided, as the condition a statement takes.
164fn owned<'a>(reach: &'a Reach, subject: &'a Value) -> Option<Owned<'a>> {
165    match reach {
166        Reach::Everything => None,
167        Reach::OwnedBy { column, .. } => Some(Owned { column, subject }),
168    }
169}
170
171/// The subject a scope matches on, as JSON, so it can be bound.
172fn subject_value(reach: &Reach) -> Value {
173    match reach {
174        Reach::Everything => Value::Null,
175        Reach::OwnedBy { subject, .. } => Value::String(subject.clone()),
176    }
177}
178
179/// What a caller asked a page for.
180///
181/// A struct because the four of them are one thing — which rows, in what
182/// order, from where — and because `page` had grown to eight positional
183/// arguments, three of them optional and adjacent. A call site that
184/// swapped two would have compiled.
185#[derive(Debug, Clone, Copy)]
186pub struct Asked<'a> {
187    /// The declared table.
188    pub table: &'a str,
189    /// The cursor from a previous page.
190    pub after: Option<&'a Value>,
191    /// What to narrow by.
192    pub filters: &'a [cratefield_tables::Filter],
193    /// What to order by.
194    pub sort: Option<cratefield_tables::Sort<'a>>,
195}
196
197/// A page of one table.
198///
199/// # Errors
200///
201/// The access decision's refusal, or a database or decoding failure.
202pub async fn page(
203    tables: &Tables,
204    conn: &dyn Database,
205    tenancy: Tenancy,
206    headers: &HeaderMap,
207    scope: &Scope,
208    asked: Asked<'_>,
209) -> Result<Value, Problem> {
210    let Asked {
211        table: name,
212        after,
213        filters,
214        sort,
215    } = asked;
216    let api = tables.find(name)?;
217    let caller = who(tables, headers, api).await?;
218    let reach = may_read(
219        api,
220        tenancy,
221        &caller,
222        require_admin(tables.ctx.config.as_ref(), headers),
223    )?;
224    let subject = subject_value(&reach);
225
226    let statement = cratefield_tables::select_page(
227        &api.table,
228        cratefield_tables::Page {
229            limit: PAGE,
230            after,
231            // The scope first, and the filters after: both stand, so a
232            // caller filtering on the subject column narrows their own
233            // rows and cannot reach anybody else's.
234            owned: owned(&reach, &subject),
235            filters,
236            sort,
237        },
238    )
239    .map_err(|err| bad_page(scope, &err, api, sort))?;
240    let rows = conn
241        .query(&statement)
242        .await
243        .map_err(|err| unavailable(scope, &err))?;
244
245    let mut out = Vec::with_capacity(rows.len());
246    for row in &rows.rows {
247        out.push(cratefield_tables::row_json(&api.table, row).map_err(|err| drifted(scope, &err))?);
248    }
249    // Only on a full page. A cursor handed back with a short one says
250    // "there is more" when there is not, and a client that believes it
251    // makes a request whose whole result is learning that.
252    let next = if out.len() as u64 == PAGE {
253        next_cursor(&api.table, out.last(), sort)
254    } else {
255        Value::Null
256    };
257    Ok(json!({ "rows": out, "next": next }))
258}
259
260/// One row by primary key.
261///
262/// # Errors
263///
264/// [`NO_SUCH_ROW`] when there is no such row **or** it belongs to
265/// somebody else — the same answer on purpose, because a distinguishable
266/// one confirms a row the caller was never in a position to learn exists.
267pub async fn one(
268    tables: &Tables,
269    conn: &dyn Database,
270    tenancy: Tenancy,
271    headers: &HeaderMap,
272    scope: &Scope,
273    name: &str,
274    key: &Value,
275) -> Result<Value, Problem> {
276    let api = tables.find(name)?;
277    let caller = who(tables, headers, api).await?;
278    let reach = may_read(
279        api,
280        tenancy,
281        &caller,
282        require_admin(tables.ctx.config.as_ref(), headers),
283    )?;
284    let subject = subject_value(&reach);
285
286    let statement = cratefield_tables::select_one(&api.table, key, owned(&reach, &subject))
287        .map_err(|err| misdeclared(scope, &err))?;
288    let rows = conn
289        .query(&statement)
290        .await
291        .map_err(|err| unavailable(scope, &err))?;
292    let row = rows.first().ok_or_else(|| Problem::new(&NO_SUCH_ROW))?;
293    cratefield_tables::row_json(&api.table, row).map_err(|err| drifted(scope, &err))
294}
295
296/// The key of the last row on the page, for the next request's cursor.
297///
298/// The caller decides whether to ask; this only builds it.
299fn next_cursor(
300    table: &TableDef,
301    last: Option<&Value>,
302    sort: Option<cratefield_tables::Sort<'_>>,
303) -> Value {
304    let Some(last) = last else {
305        return Value::Null;
306    };
307    let mut cursor = serde_json::Map::new();
308    // The sort column first, because the cursor walks the ordering the
309    // page is in. One carrying only the key resumes in key order, which
310    // silently reshuffles everything after the first page.
311    if let Some(sort) = sort {
312        let Some(value) = last.get(sort.column) else {
313            return Value::Null;
314        };
315        cursor.insert(sort.column.to_owned(), value.clone());
316    }
317    for column in &table.primary_key {
318        let Some(value) = last.get(column) else {
319            return Value::Null;
320        };
321        cursor.insert(column.clone(), value.clone());
322    }
323    Value::Object(cursor)
324}
325
326/// A page the caller asked for that this table cannot answer.
327///
328/// A filter naming a column the table does not have, or a value that is
329/// not that column's kind, is the caller's mistake and says so — with the
330/// column name, which they sent. A failure that names nothing else is a
331/// misdeclaration, and that is ours.
332fn bad_page(
333    scope: &Scope,
334    err: &cratefield_tables::DecodeError,
335    api: &TableApi,
336    sort: Option<cratefield_tables::Sort<'_>>,
337) -> Problem {
338    // A refusal about the sort column is a sort problem, whatever else it
339    // resembles. Labelling it `bad-filter` would be a slug a client
340    // branches on saying the wrong thing about what they sent.
341    if sort.is_some_and(|sort| sort.column == err.column) {
342        return Problem::new(&BAD_SORT).with_detail(format!("`{}` {}", err.column, err.detail));
343    }
344    if api
345        .table
346        .fields
347        .iter()
348        .any(|field| field.name == err.column)
349        || !api.table.primary_key.contains(&err.column)
350    {
351        return Problem::new(&BAD_FILTER).with_detail(format!("`{}` {}", err.column, err.detail));
352    }
353    misdeclared(scope, err)
354}
355
356/// A statement that could not be built from the declaration.
357///
358/// The detail is logged and never returned: it names columns, and a
359/// caller learning which column a table's subject is would learn part of
360/// its shape from an error.
361fn misdeclared(scope: &Scope, err: &cratefield_tables::DecodeError) -> Problem {
362    tracing::error!(error = %err, "a declared table's read could not be built");
363    Problem::new(&crate::access::MISDECLARED).instance(&scope.request_id)
364}
365
366/// The database could not answer.
367fn unavailable(scope: &Scope, err: &cratefield_core::DbError) -> Problem {
368    tracing::error!(error = %err, "a declared table's read failed");
369    Problem::internal().instance(&scope.request_id)
370}
371
372/// A row that does not match its declaration: the database has drifted.
373///
374/// A `500`, because the caller did nothing wrong and the answer would be
375/// wrong data. `fz tables drift` is the command that says what changed.
376fn drifted(scope: &Scope, err: &cratefield_tables::DecodeError) -> Problem {
377    tracing::error!(error = %err, "a row does not match the declaration; run `fz tables drift`");
378    Problem::internal().instance(&scope.request_id)
379}