1#[cfg(test)]
41mod tests;
42
43use std::collections::HashMap;
44use std::sync::Arc;
45
46use crate::application::Application;
47use crate::error::IntoResponse;
48use crate::flash::{Flash, Redirect};
49use crate::header::Header;
50use crate::middleware::Middleware;
51use crate::model::db_schema::{ColumnSchema, TableSchema};
52use crate::model::{Backend, DbPool, ModelRow, Value};
53use crate::request::Request;
54use crate::response::{Response, StatusCodeReasonPhrase, STATUS_CODE_REASON_PHRASE};
55use crate::server::ConnectionInfo;
56
57type Allow = Arc<dyn Fn(&Request) -> bool + Send + Sync>;
58
59pub const PAGE_SIZE: i64 = 25;
61
62#[derive(Clone)]
64pub struct ModelAdmin {
65 pool: DbPool,
66 prefix: String,
67 tables: Vec<String>,
68 allow: Option<Allow>,
69 title: String,
70}
71
72impl ModelAdmin {
73 pub fn new(pool: DbPool) -> Self {
75 ModelAdmin { pool, prefix: "/admin".into(), tables: Vec::new(), allow: None, title: "Admin".into() }
76 }
77
78 pub fn prefix(mut self, prefix: &str) -> Self {
80 self.prefix = format!("/{}", prefix.trim_matches('/'));
81 self
82 }
83
84 pub fn title(mut self, title: &str) -> Self {
86 self.title = title.to_string();
87 self
88 }
89
90 pub fn model<T: crate::model::Model>(self) -> Self {
92 self.table(T::table_name())
93 }
94
95 pub fn table(mut self, table: &str) -> Self {
97 if !table.is_empty() && table.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') && !self.tables.iter().any(|t| t == table) {
98 self.tables.push(table.to_string());
99 }
100 self
101 }
102
103 pub fn allow(mut self, check: impl Fn(&Request) -> bool + Send + Sync + 'static) -> Self {
107 self.allow = Some(Arc::new(check));
108 self
109 }
110
111 fn run<T: Send + 'static>(&self, work: impl std::future::Future<Output = Result<T, crate::model::DbError>> + Send + 'static) -> Result<T, String> {
112 rws_server::async_bridge::block_on_shared(work).map_err(|e| e.to_string())
113 }
114
115 fn schema(&self, table: &str) -> Result<Option<TableSchema>, String> {
116 let pool = self.pool.clone();
117 let schema = self.run(async move { pool.schema().await })?;
118 Ok(schema.tables.into_iter().find(|t| t.name.eq_ignore_ascii_case(table)))
119 }
120
121 fn route(&self, request: &Request) -> Option<Response> {
122 let path = request.request_uri.split('?').next().unwrap_or("");
123 let rest = path.strip_prefix(&self.prefix)?;
124 if !(rest.is_empty() || rest.starts_with('/')) {
125 return None;
126 }
127 if !self.allow.as_ref().is_some_and(|allow| allow(request)) {
128 return Some(crate::error::problem::problem(STATUS_CODE_REASON_PHRASE.n403_forbidden, "not allowed"));
129 }
130 let parts: Vec<&str> = rest.split('/').filter(|s| !s.is_empty()).collect();
131 let get = request.method == "GET" || request.method == "HEAD";
132 let post = request.method == "POST";
133 let page = Page { admin: self, request };
134 let answer = match (parts.as_slice(), get, post) {
135 ([], true, _) => page.index(),
136 ([table], true, _) => page.with_table(table, |p, t| p.list(t)),
137 ([table], _, true) => page.with_table(table, |p, t| p.save(t, None)),
138 ([table, "new"], true, _) => page.with_table(table, |p, t| p.form(t, None, None)),
139 ([table, id], true, _) => page.with_table(table, |p, t| p.show(t, id)),
140 ([table, id, "edit"], true, _) => page.with_table(table, |p, t| p.edit(t, id)),
141 ([table, id], _, true) => page.with_table(table, |p, t| p.save(t, Some(id))),
142 ([table, id, "delete"], _, true) => page.with_table(table, |p, t| p.delete(t, id)),
143 _ => Err((STATUS_CODE_REASON_PHRASE.n404_not_found, "no such admin page".to_string())),
144 };
145 Some(answer.unwrap_or_else(|(status, message)| page.error(status, &message)))
146 }
147}
148
149impl Middleware for ModelAdmin {
150 fn handle(&self, request: &Request, connection: &ConnectionInfo, next: &dyn Application) -> Result<Response, String> {
151 match self.route(request) {
152 Some(response) => Ok(response),
153 None => next.execute(request, connection),
154 }
155 }
156
157 fn is_passthrough(&self, request: &Request, _connection: &ConnectionInfo) -> bool {
158 !request.request_uri.starts_with(&self.prefix)
159 }
160
161 fn accepts_streamed_body(&self, request: &Request, _connection: &ConnectionInfo) -> bool {
162 !request.request_uri.starts_with(&self.prefix)
163 }
164
165 fn layer_name(&self) -> &'static str {
166 "ModelAdmin"
167 }
168}
169
170type PageResult = Result<Response, (&'static StatusCodeReasonPhrase, String)>;
171
172fn internal(e: String) -> (&'static StatusCodeReasonPhrase, String) {
173 eprintln!("[model admin] {e}");
174 (STATUS_CODE_REASON_PHRASE.n500_internal_server_error, e)
175}
176
177struct Page<'a> {
179 admin: &'a ModelAdmin,
180 request: &'a Request,
181}
182
183#[derive(Clone, Copy, PartialEq, Eq, Debug)]
185enum Kind {
186 Int,
187 Float,
188 Bool,
189 Timestamp,
190 Text,
191 Other,
193}
194
195fn kind(column: &ColumnSchema) -> Kind {
196 kind_of(&column.data_type)
197}
198
199fn kind_of(data_type: &str) -> Kind {
200 let t = data_type.to_ascii_lowercase();
201 if t.contains("bool") || t == "tinyint(1)" {
202 Kind::Bool
203 } else if t.contains("int") || t == "serial" || t == "bigserial" {
204 Kind::Int
205 } else if t.contains("real") || t.contains("double") || t.contains("float") || t.contains("numeric") || t.contains("decimal") {
206 Kind::Float
207 } else if t.contains("timestamp") || t.contains("datetime") {
208 Kind::Timestamp
209 } else if t.contains("char") || t.contains("text") || t.is_empty() || t == "string" {
210 Kind::Text
211 } else {
212 Kind::Other
213 }
214}
215
216fn quote(backend: Backend, name: &str) -> String {
217 match backend {
218 #[cfg(feature = "model-mysql")]
219 Backend::MySql => format!("`{}`", name.replace('`', "")),
220 #[allow(unreachable_patterns)]
221 _ => format!("\"{}\"", name.replace('"', "")),
222 }
223}
224
225fn placeholder(backend: Backend, n: usize, column: &ColumnSchema) -> String {
226 match backend {
227 #[cfg(feature = "model-postgres")]
228 Backend::Postgres => match kind(column) {
229 Kind::Timestamp | Kind::Other => format!("CAST(${n} AS {})", column.data_type),
230 _ => format!("${n}"),
231 },
232 #[allow(unreachable_patterns)]
233 _ => {
234 let _ = (n, column);
235 "?".to_string()
236 }
237 }
238}
239
240fn escape(text: &str) -> String {
241 text.replace('&', "&").replace('<', "<").replace('>', ">").replace('"', """)
242}
243
244fn display(value: &Value) -> String {
245 match value {
246 Value::Null => String::new(),
247 Value::Bool(b) => b.to_string(),
248 Value::Int(n) => n.to_string(),
249 Value::Float(f) => f.to_string(),
250 Value::Text(s) => s.clone(),
251 Value::Bytes(b) => format!("({} bytes)", b.len()),
252 }
253}
254
255fn cell(row: &ModelRow, column: &str) -> Value {
256 row.columns.iter().find(|(c, _)| c.eq_ignore_ascii_case(column)).map(|(_, v)| v.clone()).unwrap_or(Value::Null)
257}
258
259fn parse(column: &ColumnSchema, value: Option<&str>) -> Result<Value, String> {
261 parse_as(&column.name, &column.data_type, column.nullable, value)
262}
263
264fn parse_as(name: &str, data_type: &str, nullable: bool, value: Option<&str>) -> Result<Value, String> {
265 let k = kind_of(data_type);
266 if k == Kind::Bool {
267 return Ok(Value::Bool(matches!(value, Some("on" | "true" | "1"))));
269 }
270 let text = value.unwrap_or("").trim();
271 if text.is_empty() {
272 return if nullable { Ok(Value::Null) } else if k == Kind::Text { Ok(Value::Text(String::new())) } else { Err(format!("{} is required", name)) };
273 }
274 match k {
275 Kind::Int => text.parse().map(Value::Int).map_err(|_| format!("{} must be a whole number", name)),
276 Kind::Float => text.parse().map(Value::Float).map_err(|_| format!("{} must be a number", name)),
277 _ => Ok(Value::Text(value.unwrap_or("").to_string())),
278 }
279}
280
281impl<'a> Page<'a> {
282 fn backend(&self) -> Backend {
283 self.admin.pool.backend()
284 }
285
286 fn with_table(&self, table: &str, f: impl FnOnce(&Self, &TableSchema) -> PageResult) -> PageResult {
287 if !self.admin.tables.iter().any(|t| t == table) {
288 return Err((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("{table} isn't managed here")));
289 }
290 match self.admin.schema(table).map_err(internal)? {
291 Some(schema) => f(self, &schema),
292 None => Err((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("the database has no table {table}"))),
293 }
294 }
295
296 fn pk<'t>(&self, table: &'t TableSchema) -> Option<&'t ColumnSchema> {
297 match table.primary_key.as_slice() {
298 [one] => table.column(one),
299 _ => None,
300 }
301 }
302
303 fn query(&self, sql: String, params: Vec<Value>) -> Result<Vec<ModelRow>, String> {
304 let pool = self.admin.pool.clone();
305 self.admin.run(async move { pool.query_rows(&sql, ¶ms).await })
306 }
307
308 fn execute(&self, sql: String, params: Vec<Value>) -> Result<u64, String> {
309 let pool = self.admin.pool.clone();
310 self.admin.run(async move { pool.execute(&sql, ¶ms).await })
311 }
312
313 fn url(&self, parts: &[&str]) -> String {
314 let mut url = self.admin.prefix.clone();
315 for part in parts {
316 url.push('/');
317 url.push_str(&crate::router::build_path("/{x}", &[("x", part)]).unwrap_or_default()[1..]);
318 }
319 url
320 }
321
322 fn query_param(&self, name: &str) -> String {
323 let query = self.request.request_uri.split_once('?').map(|(_, q)| q).unwrap_or("");
324 form_fields(query).remove(name).unwrap_or_default()
325 }
326
327 fn html(&self, status: &StatusCodeReasonPhrase, heading: &str, body: &str) -> Response {
328 let tables: String = self.admin.tables.iter().map(|t| format!("<a href=\"{}\">{}</a>", escape(&self.url(&[t])), escape(t))).collect::<Vec<_>>().join(" ");
329 let flash: String = Flash::read(self.request).messages().iter().map(|m| format!("<p class=\"{}\">{}</p>", escape(&m.kind), escape(&m.text))).collect();
330 let page = format!(
331 "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"><title>{heading} · {title}</title>\
332 <style>body{{font:15px system-ui,sans-serif;margin:0;color:#1d1b20}}header{{background:#1d1b20;color:#fff;padding:.75rem 1rem}}header a{{color:#cfc;margin-right:1rem}}main{{padding:1rem;max-width:72rem}}\
333 table{{border-collapse:collapse;width:100%}}td,th{{border-bottom:1px solid #ddd;padding:.4rem .5rem;text-align:left;vertical-align:top;max-width:24rem;overflow-wrap:anywhere}}\
334 label{{display:block;margin:.6rem 0}}input[type=text],textarea{{display:block;width:100%;max-width:36rem;padding:.4rem;box-sizing:border-box}}.alert{{color:#b00020}}.notice{{color:#1b5e20}}button{{padding:.4rem .9rem}}form.inline{{display:inline}}</style></head>\
335 <body><header><a href=\"{home}\"><strong>{title}</strong></a>{tables}</header><main><h1>{heading}</h1>{flash}{body}</main></body></html>",
336 title = escape(&self.admin.title),
337 home = escape(&self.admin.prefix),
338 heading = escape(heading),
339 );
340 let content = crate::range::Range::get_content_range(page.into_bytes(), crate::mime_type::MimeType::TEXT_HTML.to_string());
341 let mut response = Response::get_response(status, None, Some(vec![content]));
342 response.headers.push(Header::new(Header::_CACHE_CONTROL, "no-store"));
343 response
344 }
345
346 fn error(&self, status: &'static StatusCodeReasonPhrase, message: &str) -> Response {
347 self.html(status, &format!("{} {}", status.status_code, status.reason_phrase), &format!("<p>{}</p>", escape(message)))
348 }
349
350 fn csrf(&self) -> String {
351 #[cfg(feature = "csrf")]
352 if let Some(token) = crate::csrf::CsrfToken::from_request(self.request) {
353 return format!("<input type=\"hidden\" name=\"_csrf\" value=\"{}\">", escape(token.value()));
354 }
355 String::new()
356 }
357
358 fn index(&self) -> PageResult {
359 let mut rows = String::new();
360 for table in &self.admin.tables {
361 let count = match self.query(format!("SELECT COUNT(*) AS n FROM {}", quote(self.backend(), table)), Vec::new()) {
362 Ok(r) => r.first().map(|r| display(&cell(r, "n"))).unwrap_or_default(),
363 Err(_) => "(missing)".into(),
364 };
365 rows.push_str(&format!("<tr><td><a href=\"{}\">{}</a></td><td>{}</td></tr>", escape(&self.url(&[table])), escape(table), escape(&count)));
366 }
367 Ok(self.html(STATUS_CODE_REASON_PHRASE.n200_ok, &self.admin.title, &format!("<table><tr><th>Table</th><th>Rows</th></tr>{rows}</table>")))
368 }
369
370 fn list(&self, table: &TableSchema) -> PageResult {
371 let backend = self.backend();
372 let page: i64 = self.query_param("page").parse().unwrap_or(1).max(1);
373 let q = self.query_param("q");
374 let text_columns: Vec<&ColumnSchema> = table.columns.iter().filter(|c| kind(c) == Kind::Text).collect();
375 let (mut filter, mut params) = (String::new(), Vec::new());
376 if !q.is_empty() && !text_columns.is_empty() {
377 let conditions: Vec<String> = text_columns
378 .iter()
379 .enumerate()
380 .map(|(i, c)| format!("LOWER({}) LIKE LOWER({})", quote(backend, &c.name), placeholder(backend, i + 1, c)))
381 .collect();
382 filter = format!(" WHERE {}", conditions.join(" OR "));
383 params = text_columns.iter().map(|_| Value::Text(format!("%{q}%"))).collect();
384 }
385 let order = self.pk(table).map(|pk| format!(" ORDER BY {} DESC", quote(backend, &pk.name))).unwrap_or_default();
386 let t = quote(backend, &table.name);
387 let total = self.query(format!("SELECT COUNT(*) AS n FROM {t}{filter}"), params.clone()).map_err(internal)?;
388 let total = match total.first().map(|r| cell(r, "n")) {
389 Some(Value::Int(n)) => n,
390 _ => 0,
391 };
392 let rows = self
393 .query(format!("SELECT * FROM {t}{filter}{order} LIMIT {PAGE_SIZE} OFFSET {}", (page - 1) * PAGE_SIZE), params)
394 .map_err(internal)?;
395 let pk = self.pk(table);
396 let head: String = table.columns.iter().map(|c| format!("<th>{}</th>", escape(&c.name))).collect();
397 let mut body = String::new();
398 for row in &rows {
399 body.push_str("<tr>");
400 for c in &table.columns {
401 let mut text = display(&cell(row, &c.name));
402 if text.chars().count() > 80 {
403 text = text.chars().take(80).collect::<String>() + "…";
404 }
405 match pk {
406 Some(pk) if pk.name == c.name => {
407 body.push_str(&format!("<td><a href=\"{}\">{}</a></td>", escape(&self.url(&[&table.name, &display(&cell(row, &pk.name))])), escape(&text)))
408 }
409 _ => body.push_str(&format!("<td>{}</td>", escape(&text))),
410 }
411 }
412 body.push_str("</tr>");
413 }
414 let pages = ((total + PAGE_SIZE - 1) / PAGE_SIZE).max(1);
415 let link = |p: i64| {
416 let path = crate::router::build_path(&self.url(&[&table.name]), &[("page", &p.to_string()), ("q", &q)]).unwrap_or_default();
417 escape(&path)
418 };
419 let mut nav = format!("<p>{total} rows · page {page} of {pages}");
420 if page > 1 {
421 nav.push_str(&format!(" · <a href=\"{}\">newer</a>", link(page - 1)));
422 }
423 if page < pages {
424 nav.push_str(&format!(" · <a href=\"{}\">older</a>", link(page + 1)));
425 }
426 nav.push_str("</p>");
427 let new = if pk.is_some() { format!("<p><a href=\"{}\">New {}</a></p>", escape(&self.url(&[&table.name, "new"])), escape(&table.name)) } else { "<p>(no one-column primary key: rows can't be edited here)</p>".into() };
428 let search = format!(
429 "<form method=\"get\"><input type=\"text\" name=\"q\" value=\"{}\" placeholder=\"Search\" style=\"display:inline;max-width:20rem\"> <button>Search</button></form>",
430 escape(&q)
431 );
432 Ok(self.html(STATUS_CODE_REASON_PHRASE.n200_ok, &table.name, &format!("{new}{search}{nav}<table><tr>{head}</tr>{body}</table>")))
433 }
434
435 fn find(&self, table: &TableSchema, id: &str) -> Result<ModelRow, (&'static StatusCodeReasonPhrase, String)> {
436 let pk = self.pk(table).ok_or((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("{} has no one-column primary key", table.name)))?;
437 let key = parse(pk, Some(id)).map_err(|e| (STATUS_CODE_REASON_PHRASE.n404_not_found, e))?;
438 let backend = self.backend();
439 let rows = self
440 .query(format!("SELECT * FROM {} WHERE {} = {}", quote(backend, &table.name), quote(backend, &pk.name), placeholder(backend, 1, pk)), vec![key])
441 .map_err(internal)?;
442 rows.into_iter().next().ok_or((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("no {} {id}", table.name)))
443 }
444
445 fn show(&self, table: &TableSchema, id: &str) -> PageResult {
446 let row = self.find(table, id)?;
447 let fields: String = table.columns.iter().map(|c| format!("<tr><th>{}</th><td>{}</td></tr>", escape(&c.name), escape(&display(&cell(&row, &c.name))))).collect();
448 let actions = format!(
449 "<p><a href=\"{edit}\">Edit</a> · <a href=\"{list}\">All {t}</a></p><form class=\"inline\" method=\"post\" action=\"{delete}\" onsubmit=\"return confirm('Delete this row?')\">{csrf}<button>Delete</button></form>",
450 edit = escape(&self.url(&[&table.name, id, "edit"])),
451 list = escape(&self.url(&[&table.name])),
452 delete = escape(&self.url(&[&table.name, id, "delete"])),
453 t = escape(&table.name),
454 csrf = self.csrf(),
455 );
456 Ok(self.html(STATUS_CODE_REASON_PHRASE.n200_ok, &format!("{} {id}", table.name), &format!("<table>{fields}</table>{actions}")))
457 }
458
459 fn edit(&self, table: &TableSchema, id: &str) -> PageResult {
460 let row = self.find(table, id)?;
461 self.form(table, Some(id), Some(&row))
462 }
463
464 fn editable(&self, table: &TableSchema, column: &ColumnSchema, editing: bool) -> bool {
466 let is_pk = self.pk(table).is_some_and(|pk| pk.name == column.name);
467 !(is_pk && (editing || kind(column) == Kind::Int))
468 }
469
470 fn form(&self, table: &TableSchema, id: Option<&str>, row: Option<&ModelRow>) -> PageResult {
471 if self.pk(table).is_none() {
472 return Err((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("{} has no one-column primary key", table.name)));
473 }
474 let mut fields = String::new();
475 for c in table.columns.iter().filter(|c| self.editable(table, c, id.is_some())) {
476 let value = row.map(|r| display(&cell(r, &c.name))).unwrap_or_default();
477 let hint = format!("{}{}", c.data_type.to_lowercase(), if c.nullable { ", optional" } else { "" });
478 let input = match kind(c) {
479 Kind::Bool => format!("<input type=\"checkbox\" name=\"{}\"{}>", escape(&c.name), if matches!(value.as_str(), "true" | "1") { " checked" } else { "" }),
480 Kind::Text if value.len() > 120 || value.contains('\n') => format!("<textarea name=\"{}\" rows=\"6\">{}</textarea>", escape(&c.name), escape(&value)),
481 _ => format!("<input type=\"text\" name=\"{}\" value=\"{}\">", escape(&c.name), escape(&value)),
482 };
483 fields.push_str(&format!("<label>{} <small>({})</small>{input}</label>", escape(&c.name), escape(&hint)));
484 }
485 let action = match id {
486 Some(id) => self.url(&[&table.name, id]),
487 None => self.url(&[&table.name]),
488 };
489 let heading = match id {
490 Some(id) => format!("Edit {} {id}", table.name),
491 None => format!("New {}", table.name),
492 };
493 let body = format!("<form method=\"post\" action=\"{}\">{}{fields}<button>Save</button></form>", escape(&action), self.csrf());
494 Ok(self.html(STATUS_CODE_REASON_PHRASE.n200_ok, &heading, &body))
495 }
496
497 fn save(&self, table: &TableSchema, id: Option<&str>) -> PageResult {
498 let pk = self.pk(table).ok_or((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("{} has no one-column primary key", table.name)))?;
499 let backend = self.backend();
500 let form = form_fields(&String::from_utf8_lossy(&self.request.body));
501 let back = match id {
502 Some(id) => self.url(&[&table.name, id, "edit"]),
503 None => self.url(&[&table.name, "new"]),
504 };
505 let mut columns = Vec::new();
506 let mut values = Vec::new();
507 for c in table.columns.iter().filter(|c| self.editable(table, c, id.is_some())) {
508 let given = form.get(&c.name).map(String::as_str);
509 let stamp = matches!(kind(c), Kind::Timestamp | Kind::Text)
511 && given.is_none_or(|v| v.trim().is_empty())
512 && (c.name == "updated_at" || (c.name == "created_at" && id.is_none()));
513 if id.is_some() && given.is_none() && !stamp && kind(c) != Kind::Bool {
516 continue;
517 }
518 let value = if stamp {
519 crate::model::Timestamp::now().to_value(backend)
520 } else {
521 match parse(c, given) {
522 Ok(v) => v,
523 Err(e) => return Ok(Redirect::to(&back).alert(&e).into_response()),
524 }
525 };
526 columns.push(c);
527 values.push(value);
528 }
529 let result = match id {
530 None => {
531 let names: Vec<String> = columns.iter().map(|c| quote(backend, &c.name)).collect();
532 let marks: Vec<String> = columns.iter().enumerate().map(|(i, c)| placeholder(backend, i + 1, c)).collect();
533 let sql = if columns.is_empty() {
534 format!("INSERT INTO {} DEFAULT VALUES", quote(backend, &table.name))
535 } else {
536 format!("INSERT INTO {} ({}) VALUES ({})", quote(backend, &table.name), names.join(", "), marks.join(", "))
537 };
538 self.execute(sql, values).map(|_| None)
539 }
540 Some(id) => {
541 let key = parse(pk, Some(id)).map_err(|e| (STATUS_CODE_REASON_PHRASE.n404_not_found, e))?;
542 let sets: Vec<String> = columns.iter().enumerate().map(|(i, c)| format!("{} = {}", quote(backend, &c.name), placeholder(backend, i + 1, c))).collect();
543 let sql = format!("UPDATE {} SET {} WHERE {} = {}", quote(backend, &table.name), sets.join(", "), quote(backend, &pk.name), placeholder(backend, columns.len() + 1, pk));
544 values.push(key);
545 self.execute(sql, values).map(|n| Some((n, id.to_string())))
546 }
547 };
548 match result {
549 Err(e) => Ok(Redirect::to(&back).alert(&format!("The database refused it: {e}")).into_response()),
550 Ok(Some((0, id))) => Err((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("no {} {id}", table.name))),
551 Ok(Some((_, id))) => Ok(Redirect::to(&self.url(&[&table.name, &id])).notice("Saved.").into_response()),
552 Ok(None) => Ok(Redirect::to(&self.url(&[&table.name])).notice("Created.").into_response()),
553 }
554 }
555
556 fn delete(&self, table: &TableSchema, id: &str) -> PageResult {
557 let pk = self.pk(table).ok_or((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("{} has no one-column primary key", table.name)))?;
558 let backend = self.backend();
559 let key = parse(pk, Some(id)).map_err(|e| (STATUS_CODE_REASON_PHRASE.n404_not_found, e))?;
560 let sql = format!("DELETE FROM {} WHERE {} = {}", quote(backend, &table.name), quote(backend, &pk.name), placeholder(backend, 1, pk));
561 match self.execute(sql, vec![key]) {
562 Ok(0) => Err((STATUS_CODE_REASON_PHRASE.n404_not_found, format!("no {} {id}", table.name))),
563 Ok(_) => Ok(Redirect::to(&self.url(&[&table.name])).notice("Deleted.").into_response()),
564 Err(e) => Ok(Redirect::to(&self.url(&[&table.name, id])).alert(&format!("The database refused it: {e}")).into_response()),
565 }
566 }
567}
568
569fn form_fields(text: &str) -> HashMap<String, String> {
571 fn decode(text: &str) -> Option<String> {
572 let bytes = text.as_bytes();
573 let mut out = Vec::with_capacity(bytes.len());
574 let mut i = 0;
575 while i < bytes.len() {
576 match bytes[i] {
577 b'+' => {
578 out.push(b' ');
579 i += 1;
580 }
581 b'%' => {
582 out.push(u8::from_str_radix(text.get(i + 1..i + 3)?, 16).ok()?);
583 i += 3;
584 }
585 b => {
586 out.push(b);
587 i += 1;
588 }
589 }
590 }
591 String::from_utf8(out).ok()
592 }
593 text.split('&')
594 .filter(|p| !p.is_empty())
595 .filter_map(|pair| {
596 let (k, v) = pair.split_once('=').unwrap_or((pair, ""));
597 Some((decode(k)?, decode(v)?))
598 })
599 .collect()
600}