trailbase_sqlite/
schema.rs

1use jsonschema::Validator;
2use lazy_static::lazy_static;
3use schemars::{schema_for, JsonSchema};
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::sync::Arc;
7use thiserror::Error;
8use trailbase_extension::jsonschema::{SchemaEntry, ValidationError};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Error)]
12pub enum SchemaError {
13  #[error("JSONSchema validation error: {0}")]
14  JsonSchema(Arc<ValidationError>),
15  #[error("Cannot update builtin schemas")]
16  BuiltinSchema,
17  #[error("Missing name")]
18  MissingName,
19}
20
21/// File input schema used both for multipart-form uploads (in which case the name is mapped to
22/// column names) and JSON where the column name is extracted from the corresponding key of the
23/// parent object.
24#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
25#[serde(deny_unknown_fields)]
26pub struct FileUploadInput {
27  /// The name of the form's file control.
28  pub name: Option<String>,
29
30  /// The file's file name.
31  pub filename: Option<String>,
32
33  /// The file's content type.
34  pub content_type: Option<String>,
35
36  /// The file's data
37  pub data: Vec<u8>,
38}
39
40impl FileUploadInput {
41  pub fn consume(self) -> Result<(Option<String>, FileUpload, Vec<u8>), SchemaError> {
42    // We don't trust user provided type, we check ourselves.
43    let mime_type = infer::get(&self.data).map(|t| t.mime_type().to_string());
44
45    return Ok((
46      self.name,
47      FileUpload::new(
48        uuid::Uuid::new_v4(),
49        self.filename,
50        self.content_type,
51        mime_type,
52      ),
53      self.data,
54    ));
55  }
56}
57
58#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
59#[serde(deny_unknown_fields)]
60pub struct FileUpload {
61  // The file's unique is from with the path is derived.
62  id: String,
63
64  /// The file's original file name.
65  filename: Option<String>,
66
67  /// The file's user-provided content type.
68  content_type: Option<String>,
69
70  /// The file's inferred mime type. Not user provided.
71  mime_type: Option<String>,
72}
73
74impl FileUpload {
75  pub fn new(
76    id: Uuid,
77    filename: Option<String>,
78    content_type: Option<String>,
79    mime_type: Option<String>,
80  ) -> Self {
81    Self {
82      id: id.to_string(),
83      filename,
84      content_type,
85      mime_type,
86    }
87  }
88
89  pub fn path(&self) -> &str {
90    &self.id
91  }
92
93  pub fn content_type(&self) -> Option<&str> {
94    self.content_type.as_deref()
95  }
96
97  pub fn original_filename(&self) -> Option<&str> {
98    self.filename.as_deref()
99  }
100}
101
102#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
103pub struct FileUploads(pub Vec<FileUpload>);
104
105fn builtin_schemas() -> &'static HashMap<String, SchemaEntry> {
106  fn validate_mime_type(value: &serde_json::Value, extra_args: Option<&str>) -> bool {
107    let Some(valid_mime_types) = extra_args else {
108      return true;
109    };
110
111    if let serde_json::Value::Object(ref map) = value {
112      if let Some(serde_json::Value::String(mime_type)) = map.get("mime_type") {
113        if valid_mime_types.contains(mime_type) {
114          return true;
115        }
116      }
117    }
118
119    return false;
120  }
121
122  lazy_static! {
123    static ref builtins: HashMap<String, SchemaEntry> = HashMap::<String, SchemaEntry>::from([
124      (
125        "std.FileUpload".to_string(),
126        SchemaEntry::from(
127          serde_json::to_value(schema_for!(FileUpload)).unwrap(),
128          Some(Arc::new(validate_mime_type))
129        )
130        .unwrap()
131      ),
132      (
133        "std.FileUploads".to_string(),
134        SchemaEntry::from(
135          serde_json::to_value(schema_for!(FileUploads)).unwrap(),
136          None
137        )
138        .unwrap(),
139      )
140    ]);
141  }
142
143  return &builtins;
144}
145
146#[derive(Debug, Clone)]
147pub struct Schema {
148  pub name: String,
149  pub schema: serde_json::Value,
150  pub builtin: bool,
151}
152
153pub fn get_schema(name: &str) -> Option<Schema> {
154  let builtins = builtin_schemas();
155
156  trailbase_extension::jsonschema::get_schema(name).map(|s| Schema {
157    name: name.to_string(),
158    schema: s,
159    builtin: builtins.contains_key(name),
160  })
161}
162
163pub fn get_compiled_schema(name: &str) -> Option<Arc<Validator>> {
164  trailbase_extension::jsonschema::get_compiled_schema(name)
165}
166
167pub fn get_schemas() -> Vec<Schema> {
168  let builtins = builtin_schemas();
169  return trailbase_extension::jsonschema::get_schemas()
170    .into_iter()
171    .map(|(name, value)| {
172      let builtin = builtins.contains_key(&name);
173      return Schema {
174        name,
175        schema: value,
176        builtin,
177      };
178    })
179    .collect();
180}
181
182pub fn set_user_schema(name: &str, pattern: Option<serde_json::Value>) -> Result<(), SchemaError> {
183  let builtins = builtin_schemas();
184  if builtins.contains_key(name) {
185    return Err(SchemaError::BuiltinSchema);
186  }
187
188  if let Some(p) = pattern {
189    let entry = SchemaEntry::from(p, None).map_err(|err| SchemaError::JsonSchema(Arc::new(err)))?;
190    trailbase_extension::jsonschema::set_schema(name, Some(entry));
191  } else {
192    trailbase_extension::jsonschema::set_schema(name, None);
193  }
194
195  return Ok(());
196}
197
198lazy_static! {
199  static ref INIT: std::sync::Mutex<bool> = std::sync::Mutex::new(false);
200}
201
202pub fn set_user_schemas(schemas: Vec<(String, serde_json::Value)>) -> Result<(), SchemaError> {
203  let mut entries: Vec<(String, SchemaEntry)> = vec![];
204  for (name, entry) in builtin_schemas() {
205    entries.push((name.clone(), entry.clone()));
206  }
207
208  for (name, schema) in schemas {
209    entries.push((
210      name,
211      SchemaEntry::from(schema, None).map_err(|err| SchemaError::JsonSchema(Arc::new(err)))?,
212    ));
213  }
214
215  trailbase_extension::jsonschema::set_schemas(Some(entries));
216
217  *INIT.lock().unwrap() = true;
218
219  return Ok(());
220}
221
222pub(crate) fn try_init_schemas() {
223  let mut init = INIT.lock().unwrap();
224  if !*init {
225    let entries = builtin_schemas()
226      .iter()
227      .map(|(name, entry)| (name.clone(), entry.clone()))
228      .collect::<Vec<_>>();
229
230    trailbase_extension::jsonschema::set_schemas(Some(entries));
231    *init = true;
232  }
233}
234
235#[cfg(test)]
236mod tests {
237  use serde_json::json;
238
239  use super::*;
240
241  #[test]
242  fn test_builtin_schemas() {
243    assert!(builtin_schemas().len() > 0);
244
245    for (name, schema) in builtin_schemas() {
246      trailbase_extension::jsonschema::set_schema(&name, Some(schema.clone()));
247    }
248
249    {
250      let schema = get_schema("std.FileUpload").unwrap();
251      let compiled_schema = Validator::new(&schema.schema).unwrap();
252      let input = json!({
253        "id": "foo",
254        "mime_type": "my_foo",
255      });
256      if let Err(err) = compiled_schema.validate(&input) {
257        panic!("{err:?}");
258      };
259    }
260
261    {
262      let schema = get_schema("std.FileUploads").unwrap();
263      let compiled_schema = Validator::new(&schema.schema).unwrap();
264      assert!(compiled_schema
265        .validate(&json!([
266          {
267            "id": "foo0",
268            "mime_type": "my_foo0",
269          },
270          {
271            "id": "foo1",
272            "mime_type": "my_foo1",
273          },
274        ]))
275        .is_ok());
276    }
277  }
278}