1use 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
40pub const PAGE: u64 = 50;
45
46pub 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
54pub 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
63pub 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
71pub 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
79pub 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
87pub 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
95pub const NOT_VERIFIED: ProblemDef = cratefield_core::SLUGS.unauthenticated;
100
101pub struct Tables {
103 pub tables: Vec<TableApi>,
105 pub ctx: Arc<ModuleContext>,
107}
108
109impl Tables {
110 #[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
124pub(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 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 cratefield_core::AuthError::Unavailable(_) => Problem::new(&VERIFIER_UNAVAILABLE),
152 other => {
157 tracing::error!(error = %other, "a refusal this build does not understand");
158 Problem::new(&VERIFIER_UNAVAILABLE)
159 }
160 })
161}
162
163fn 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
171fn 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#[derive(Debug, Clone, Copy)]
186pub struct Asked<'a> {
187 pub table: &'a str,
189 pub after: Option<&'a Value>,
191 pub filters: &'a [cratefield_tables::Filter],
193 pub sort: Option<cratefield_tables::Sort<'a>>,
195}
196
197pub 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 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 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
260pub 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
296fn 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 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
326fn bad_page(
333 scope: &Scope,
334 err: &cratefield_tables::DecodeError,
335 api: &TableApi,
336 sort: Option<cratefield_tables::Sort<'_>>,
337) -> Problem {
338 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
356fn 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
366fn 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
372fn 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}