use std::sync::Arc;
use cratefield_core::{
Caller, Database, ModuleContext, Problem, ProblemDef, Scope, Tenancy, require_admin,
};
use cratefield_tables::{Owned, TableDef};
use http::{HeaderMap, StatusCode};
use serde_json::{Value, json};
use crate::access::{Reach, TableApi, may_read};
pub const PAGE: u64 = 50;
pub const NO_SUCH_TABLE: ProblemDef = ProblemDef {
slug: "no-such-table",
status: StatusCode::NOT_FOUND,
title: "No such table",
description: "This venture declares no table by that name.",
};
pub const NO_SUCH_ROW: ProblemDef = ProblemDef {
slug: "no-such-row",
status: StatusCode::NOT_FOUND,
title: "No such row",
description: "No row of this table has that key.",
};
pub const NO_VERIFIER: ProblemDef = ProblemDef {
slug: "no-verifier",
status: StatusCode::INTERNAL_SERVER_ERROR,
title: "Cannot identify the caller",
description: "This table is not public and the deployment wired no way to verify a caller.",
};
pub const VERIFIER_UNAVAILABLE: ProblemDef = ProblemDef {
slug: "verifier-unavailable",
status: StatusCode::SERVICE_UNAVAILABLE,
title: "Cannot check the credential",
description: "The service that verifies credentials could not answer. Try again shortly.",
};
pub const BAD_SORT: ProblemDef = ProblemDef {
slug: "bad-sort",
status: StatusCode::BAD_REQUEST,
title: "Not a sort for this table",
description: "The `sort` parameter names a column the table does not declare, or one whose nulls the engines order differently.",
};
pub const BAD_FILTER: ProblemDef = ProblemDef {
slug: "bad-filter",
status: StatusCode::BAD_REQUEST,
title: "Not a filter for this table",
description: "A query parameter names a column the table does not declare, or a value that is not that column's kind.",
};
pub const NOT_VERIFIED: ProblemDef = cratefield_core::SLUGS.unauthenticated;
pub struct Tables {
pub tables: Vec<TableApi>,
pub ctx: Arc<ModuleContext>,
}
impl Tables {
#[must_use]
pub fn declared(&self, name: &str) -> Option<&TableApi> {
self.tables.iter().find(|api| api.table.name == name)
}
fn find(&self, name: &str) -> Result<&TableApi, Problem> {
self.tables
.iter()
.find(|api| api.table.name == name)
.ok_or_else(|| Problem::new(&NO_SUCH_TABLE))
}
}
pub(crate) async fn who(
tables: &Tables,
headers: &HeaderMap,
api: &TableApi,
) -> Result<Caller, Problem> {
let Some(auth) = tables.ctx.ports.auth.as_ref() else {
if api.access == cratefield_manifest::Access::PublicRead {
return Ok(Caller::Anonymous);
}
return Err(Problem::new(&NO_VERIFIER));
};
auth.identify(headers).await.map_err(|err| match err {
cratefield_core::AuthError::NotVerified => Problem::new(&NOT_VERIFIED),
cratefield_core::AuthError::Unavailable(_) => Problem::new(&VERIFIER_UNAVAILABLE),
other => {
tracing::error!(error = %other, "a refusal this build does not understand");
Problem::new(&VERIFIER_UNAVAILABLE)
}
})
}
fn owned<'a>(reach: &'a Reach, subject: &'a Value) -> Option<Owned<'a>> {
match reach {
Reach::Everything => None,
Reach::OwnedBy { column, .. } => Some(Owned { column, subject }),
}
}
fn subject_value(reach: &Reach) -> Value {
match reach {
Reach::Everything => Value::Null,
Reach::OwnedBy { subject, .. } => Value::String(subject.clone()),
}
}
#[derive(Debug, Clone, Copy)]
pub struct Asked<'a> {
pub table: &'a str,
pub after: Option<&'a Value>,
pub filters: &'a [cratefield_tables::Filter],
pub sort: Option<cratefield_tables::Sort<'a>>,
}
pub async fn page(
tables: &Tables,
conn: &dyn Database,
tenancy: Tenancy,
headers: &HeaderMap,
scope: &Scope,
asked: Asked<'_>,
) -> Result<Value, Problem> {
let Asked {
table: name,
after,
filters,
sort,
} = asked;
let api = tables.find(name)?;
let caller = who(tables, headers, api).await?;
let reach = may_read(
api,
tenancy,
&caller,
require_admin(tables.ctx.config.as_ref(), headers),
)?;
let subject = subject_value(&reach);
let statement = cratefield_tables::select_page(
&api.table,
cratefield_tables::Page {
limit: PAGE,
after,
owned: owned(&reach, &subject),
filters,
sort,
},
)
.map_err(|err| bad_page(scope, &err, api, sort))?;
let rows = conn
.query(&statement)
.await
.map_err(|err| unavailable(scope, &err))?;
let mut out = Vec::with_capacity(rows.len());
for row in &rows.rows {
out.push(cratefield_tables::row_json(&api.table, row).map_err(|err| drifted(scope, &err))?);
}
let next = if out.len() as u64 == PAGE {
next_cursor(&api.table, out.last(), sort)
} else {
Value::Null
};
Ok(json!({ "rows": out, "next": next }))
}
pub async fn one(
tables: &Tables,
conn: &dyn Database,
tenancy: Tenancy,
headers: &HeaderMap,
scope: &Scope,
name: &str,
key: &Value,
) -> Result<Value, Problem> {
let api = tables.find(name)?;
let caller = who(tables, headers, api).await?;
let reach = may_read(
api,
tenancy,
&caller,
require_admin(tables.ctx.config.as_ref(), headers),
)?;
let subject = subject_value(&reach);
let statement = cratefield_tables::select_one(&api.table, key, owned(&reach, &subject))
.map_err(|err| misdeclared(scope, &err))?;
let rows = conn
.query(&statement)
.await
.map_err(|err| unavailable(scope, &err))?;
let row = rows.first().ok_or_else(|| Problem::new(&NO_SUCH_ROW))?;
cratefield_tables::row_json(&api.table, row).map_err(|err| drifted(scope, &err))
}
fn next_cursor(
table: &TableDef,
last: Option<&Value>,
sort: Option<cratefield_tables::Sort<'_>>,
) -> Value {
let Some(last) = last else {
return Value::Null;
};
let mut cursor = serde_json::Map::new();
if let Some(sort) = sort {
let Some(value) = last.get(sort.column) else {
return Value::Null;
};
cursor.insert(sort.column.to_owned(), value.clone());
}
for column in &table.primary_key {
let Some(value) = last.get(column) else {
return Value::Null;
};
cursor.insert(column.clone(), value.clone());
}
Value::Object(cursor)
}
fn bad_page(
scope: &Scope,
err: &cratefield_tables::DecodeError,
api: &TableApi,
sort: Option<cratefield_tables::Sort<'_>>,
) -> Problem {
if sort.is_some_and(|sort| sort.column == err.column) {
return Problem::new(&BAD_SORT).with_detail(format!("`{}` {}", err.column, err.detail));
}
if api
.table
.fields
.iter()
.any(|field| field.name == err.column)
|| !api.table.primary_key.contains(&err.column)
{
return Problem::new(&BAD_FILTER).with_detail(format!("`{}` {}", err.column, err.detail));
}
misdeclared(scope, err)
}
fn misdeclared(scope: &Scope, err: &cratefield_tables::DecodeError) -> Problem {
tracing::error!(error = %err, "a declared table's read could not be built");
Problem::new(&crate::access::MISDECLARED).instance(&scope.request_id)
}
fn unavailable(scope: &Scope, err: &cratefield_core::DbError) -> Problem {
tracing::error!(error = %err, "a declared table's read failed");
Problem::internal().instance(&scope.request_id)
}
fn drifted(scope: &Scope, err: &cratefield_tables::DecodeError) -> Problem {
tracing::error!(error = %err, "a row does not match the declaration; run `fz tables drift`");
Problem::internal().instance(&scope.request_id)
}