use parse_rust_core::{ClassLevelPermissions, ErrorCode, ParseError, ParseMap, ParseValue};
use parse_rust_schema::{plan_update, validate_new_class};
use parse_rust_storage::{AddFieldOutcome, ClassSchema, FieldType, Query, StorageAdapter};
use serde_json::{json, Value as Json};
use crate::state::AppState;
pub async fn get_all(state: &AppState) -> Result<Json, ParseError> {
let classes = state.storage().all_schemas().await?;
Ok(json!({
"results": classes.iter().map(render).collect::<Vec<_>>(),
}))
}
pub async fn get_one(state: &AppState, class_name: &str) -> Result<Json, ParseError> {
let schema = load_one(state, class_name).await?;
Ok(render(&schema))
}
pub async fn create(
state: &AppState,
path: &str,
class_name: Option<&str>,
body: &Json,
) -> Result<Json, ParseError> {
let body = as_object(body)?;
let body_class = match class_name_field(&body) {
ClassNameField::Absent => None,
ClassNameField::Named(name) => Some(name),
ClassNameField::Malformed(rendered) => {
return Err(mismatch(&rendered, class_name.unwrap_or(path)))
}
};
if let (Some(path_class), Some(body_class)) = (class_name, body_class.as_deref()) {
if path_class != body_class {
return Err(mismatch(body_class, path_class));
}
}
let Some(class_name) = class_name.or(body_class.as_deref()) else {
return Err(ParseError::new(
ErrorCode::MissingClassName,
format!("POST {path} needs a class name."),
));
};
let fields = fields_block(&body, class_name)?;
let clp = clp_block(&body, class_name)?;
let mut schema = validate_new_class(class_name, &fields, clp, state.config().clp_validation())?;
schema.indexes = apply_indexes(
state,
class_name,
index_block(&body, class_name)?.as_ref(),
None,
&schema,
true,
)
.await?;
state.storage().insert_schema(&schema).await.map_err(|e| {
if e.code == ErrorCode::DuplicateValue {
ParseError::new(
ErrorCode::InvalidClassName,
format!("Class {class_name} already exists."),
)
} else {
e
}
})?;
Ok(render(&schema))
}
pub async fn update(state: &AppState, class_name: &str, body: &Json) -> Result<Json, ParseError> {
let body = as_object(body)?;
match class_name_field(&body) {
ClassNameField::Absent => {}
ClassNameField::Named(body_class) if body_class == class_name => {}
ClassNameField::Named(body_class) => return Err(mismatch(&body_class, class_name)),
ClassNameField::Malformed(rendered) => return Err(mismatch(&rendered, class_name)),
}
let existing = load_one(state, class_name).await?;
let fields = fields_block(&body, class_name)?;
let clp = clp_block(&body, class_name)?;
let mutation = plan_update(&existing, &fields, clp, state.config().clp_validation())?;
if !mutation.deleted.is_empty() {
state
.storage()
.delete_fields(&existing, &mutation.deleted)
.await?;
}
let mut merged = existing.clone();
for name in &mutation.deleted {
merged.fields.shift_remove(name.as_str());
}
for field in &mutation.set_fields {
merged
.fields
.insert(field.name.clone(), field.field_type.clone());
}
for field in &mutation.set_fields {
if field.is_new {
let options = (!field.options.is_empty()).then(|| field.options.clone());
match state
.storage()
.reserve_field(class_name, &field.name, &field.field_type, options.as_ref())
.await?
{
AddFieldOutcome::Added | AddFieldOutcome::AlreadyPresentSameType => {}
AddFieldOutcome::Conflict { existing } => {
return Err(parse_rust_schema::infer::schema_mismatch(
class_name,
&field.name,
&existing,
&field.field_type,
))
}
}
continue;
}
parse_rust_schema::check_default_value_type(
class_name,
&field.name,
&field.field_type,
&field.options,
)?;
state
.storage()
.set_field_options(class_name, &field.name, &field.options)
.await?;
}
if let Some(clp) = mutation.clp.clone() {
state
.storage()
.set_class_permissions(class_name, Some(&clp))
.await?;
}
if let Some(recorded) = apply_indexes(
state,
class_name,
index_block(&body, class_name)?.as_ref(),
merged.indexes.as_ref(),
&merged,
false,
)
.await?
{
state.storage().set_indexes(class_name, &recorded).await?;
}
let reloaded = load_one(state, class_name).await?;
Ok(render(&reloaded))
}
async fn apply_indexes(
state: &AppState,
class_name: &str,
submitted: Option<&ParseMap>,
existing: Option<&ParseMap>,
fields: &ClassSchema,
is_create: bool,
) -> Result<Option<ParseMap>, ParseError> {
let Some(submitted) = submitted else {
return Ok(None);
};
let mut recorded = match existing {
Some(existing) if !existing.is_empty() => existing.clone(),
_ if is_create => ParseMap::new(),
_ => {
let mut seed = ParseMap::new();
let mut id = ParseMap::new();
id.insert("_id".to_string(), ParseValue::Number(1.0));
seed.insert("_id_".to_string(), ParseValue::Object(id));
seed
}
};
let mut to_build: Vec<parse_rust_storage::SchemaIndex> = Vec::new();
let mut to_drop: Vec<String> = Vec::new();
for (name, spec) in submitted {
let is_delete = matches!(spec, ParseValue::Object(m)
if matches!(m.get("__op"), Some(ParseValue::String(op)) if op == "Delete"));
if recorded.contains_key(name.as_str()) && !is_delete {
return Err(ParseError::invalid_query(format!(
"Index {name} exists, cannot update."
)));
}
if !recorded.contains_key(name.as_str()) && is_delete {
return Err(ParseError::invalid_query(format!(
"Index {name} does not exist, cannot delete."
)));
}
if is_delete {
to_drop.push(name.clone());
recorded.shift_remove(name.as_str());
continue;
}
let ParseValue::Object(keys) = spec else {
return Err(ParseError::invalid_query(format!(
"Index {name} is not an object."
)));
};
let mut lowered = Vec::with_capacity(keys.len());
for (key, direction) in keys {
let field = key.strip_prefix("_p_").unwrap_or(key);
if fields.field(field).is_none()
&& !parse_rust_schema::infer::is_default_column(class_name, field)
{
return Err(ParseError::invalid_query(format!(
"Field {key} does not exist, cannot add index."
)));
}
lowered.push((key.clone(), direction.clone()));
}
to_build.push(parse_rust_storage::SchemaIndex {
name: name.clone(),
keys: lowered,
});
recorded.insert(name.clone(), spec.clone());
}
for name in &to_drop {
state.storage().drop_index(class_name, name).await?;
}
state
.storage()
.create_indexes(class_name, &to_build)
.await?;
Ok(Some(recorded))
}
pub async fn delete(state: &AppState, class_name: &str) -> Result<Json, ParseError> {
if !parse_rust_schema::class_name_is_valid(class_name) {
return Err(ParseError::new(
ErrorCode::InvalidClassName,
parse_rust_schema::infer::invalid_class_name_message(class_name),
));
}
let schema = match find_schema(state, class_name).await? {
Some(schema) => schema,
None => ClassSchema::new(class_name),
};
let count = state.storage().count(&schema, &Query::new()).await?;
if count > 0 {
return Err(ParseError::new(
ErrorCode::InvalidSchemaOperation,
format!(
"Class {class_name} is not empty, contains {count} objects, cannot drop schema."
),
));
}
state.storage().delete_class(&schema).await?;
Ok(json!({}))
}
pub async fn purge(state: &AppState, class_name: &str) -> Result<Json, ParseError> {
let Some(schema) = find_schema(state, class_name).await? else {
return Ok(json!({}));
};
state.storage().delete(&schema, &Query::new()).await?;
Ok(json!({}))
}
fn render(schema: &ClassSchema) -> Json {
let mut fields = serde_json::Map::new();
for (name, ty) in &schema.fields {
let mut entry = serde_json::Map::new();
entry.insert("type".to_string(), json!(ty.wire_type()));
if let Some(target) = ty.target_class() {
entry.insert("targetClass".to_string(), json!(target));
}
if let Some(ParseValue::Object(options)) = schema
.field_options
.as_ref()
.and_then(|o| o.get(name.as_str()))
{
if let Json::Object(rendered) = to_json(options) {
for (key, value) in rendered {
entry.insert(key, value);
}
}
}
fields.insert(name.clone(), Json::Object(entry));
}
for (name, ty) in [
("ACL", "ACL"),
("createdAt", "Date"),
("updatedAt", "Date"),
("objectId", "String"),
] {
if !fields.contains_key(name) {
fields.insert(name.to_string(), json!({ "type": ty }));
}
}
let mut body = json!({
"className": schema.class_name,
"fields": Json::Object(fields),
"classLevelPermissions": render_clp(schema.clp.as_ref()),
});
if let Some(indexes) = schema.indexes.as_ref().filter(|i| !i.is_empty()) {
if let Some(map) = body.as_object_mut() {
map.insert("indexes".to_string(), to_json(indexes));
}
}
body
}
fn render_clp(clp: Option<&ClassLevelPermissions>) -> Json {
match clp {
Some(clp) => to_json(clp.raw()),
None => json!({
"ACL": { "*": { "read": true, "write": true } },
"find": { "*": true },
"count": { "*": true },
"get": { "*": true },
"create": { "*": true },
"update": { "*": true },
"delete": { "*": true },
"addField": { "*": true },
"protectedFields": { "*": [] },
}),
}
}
fn to_json(map: &ParseMap) -> Json {
serde_json::from_str(&ParseValue::Object(map.clone()).to_json()).unwrap_or(Json::Null)
}
async fn find_schema(
state: &AppState,
class_name: &str,
) -> Result<Option<ClassSchema>, ParseError> {
Ok(state
.storage()
.all_schemas()
.await?
.into_iter()
.find(|s| s.class_name == class_name))
}
async fn load_one(state: &AppState, class_name: &str) -> Result<ClassSchema, ParseError> {
find_schema(state, class_name).await?.ok_or_else(|| {
ParseError::new(
ErrorCode::InvalidClassName,
format!("Class {class_name} does not exist."),
)
})
}
fn mismatch(body_class: &str, path_class: &str) -> ParseError {
ParseError::new(
ErrorCode::InvalidClassName,
format!("Class name mismatch between {body_class} and {path_class}."),
)
}
fn as_object(body: &Json) -> Result<ParseMap, ParseError> {
match parse_rust_core::classify_raw(body.clone())? {
ParseValue::Object(map) => Ok(map),
_ => Err(ParseError::invalid_json("body must be an object")),
}
}
enum ClassNameField {
Absent,
Named(String),
Malformed(String),
}
fn class_name_field(body: &ParseMap) -> ClassNameField {
match body.get("className") {
None | Some(ParseValue::Null) | Some(ParseValue::Bool(false)) => ClassNameField::Absent,
Some(ParseValue::Number(n)) if *n == 0.0 => ClassNameField::Absent,
Some(ParseValue::String(s)) if s.is_empty() => ClassNameField::Absent,
Some(ParseValue::String(s)) => ClassNameField::Named(s.clone()),
Some(other) => ClassNameField::Malformed(render_as_js_string(other)),
}
}
fn render_as_js_string(value: &ParseValue) -> String {
match value {
ParseValue::String(s) => s.clone(),
ParseValue::Number(n) => parse_rust_core::js_number::to_ecma_string(*n),
ParseValue::Bool(b) => b.to_string(),
ParseValue::Null => "null".to_string(),
ParseValue::Array(items) => items
.iter()
.map(|item| match item {
ParseValue::Null => String::new(),
other => render_as_js_string(other),
})
.collect::<Vec<_>>()
.join(","),
_ => "[object Object]".to_string(),
}
}
fn fields_block(body: &ParseMap, class_name: &str) -> Result<ParseMap, ParseError> {
match body.get("fields") {
None => Ok(ParseMap::new()),
Some(ParseValue::Object(map)) => Ok(map.clone()),
Some(_) => Err(ParseError::invalid_json(format!(
"Invalid fields for class {class_name}: expected an object."
))),
}
}
fn clp_block(body: &ParseMap, class_name: &str) -> Result<Option<ParseMap>, ParseError> {
match body.get("classLevelPermissions") {
None => Ok(None),
Some(ParseValue::Object(map)) => Ok(Some(map.clone())),
Some(_) => Err(ParseError::invalid_json(format!(
"Invalid classLevelPermissions for class {class_name}: expected an object."
))),
}
}
fn index_block(body: &ParseMap, class_name: &str) -> Result<Option<ParseMap>, ParseError> {
match body.get("indexes") {
None => Ok(None),
Some(ParseValue::Object(map)) => Ok(Some(map.clone())),
Some(_) => Err(ParseError::invalid_query(format!(
"Invalid indexes for class {class_name}: expected an object."
))),
}
}
trait WireType {
fn wire_type(&self) -> &'static str;
}
impl WireType for FieldType {
fn wire_type(&self) -> &'static str {
match self {
FieldType::String => "String",
FieldType::Number => "Number",
FieldType::Boolean => "Boolean",
FieldType::Date => "Date",
FieldType::Object => "Object",
FieldType::Array => "Array",
FieldType::GeoPoint => "GeoPoint",
FieldType::File => "File",
FieldType::Bytes => "Bytes",
FieldType::Polygon => "Polygon",
FieldType::Acl => "ACL",
FieldType::Pointer { .. } => "Pointer",
FieldType::Relation { .. } => "Relation",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absent_clp_block_and_a_present_one_render_differently() {
let absent = render_clp(None);
assert!(
absent.get("ACL").is_some(),
"defaultCLPS carries an ACL key"
);
assert_eq!(absent["find"], json!({ "*": true }));
let present = ClassLevelPermissions::from_map(
match parse_rust_core::classify(
serde_json::from_str(r#"{"find":{"*":true},"count":{},"get":{},"create":{},"update":{},"delete":{},"addField":{},"protectedFields":{}}"#)
.expect("literal"),
)
.expect("classify")
{
ParseValue::Object(m) => m,
_ => unreachable!(),
},
);
let rendered = render_clp(Some(&present));
assert!(
rendered.get("ACL").is_none(),
"emptyCLPS has no ACL key, so a present block never grows one"
);
assert_eq!(
rendered["count"],
json!({}),
"unspecified operations are {{}}"
);
}
#[test]
fn a_rendered_schema_carries_the_four_implicit_columns() {
let schema = ClassSchema::new("Post").with_field("title", FieldType::String);
let rendered = render(&schema);
assert_eq!(rendered["className"], json!("Post"));
assert_eq!(rendered["fields"]["title"], json!({ "type": "String" }));
assert_eq!(rendered["fields"]["ACL"], json!({ "type": "ACL" }));
assert_eq!(rendered["fields"]["objectId"], json!({ "type": "String" }));
assert!(
rendered.get("indexes").is_none(),
"a class with no indexes must not carry the key: {rendered}"
);
}
#[test]
fn a_class_with_indexes_renders_them() {
let mut schema = ClassSchema::new("Post").with_field("title", FieldType::String);
let mut indexes = ParseMap::new();
let mut key = ParseMap::new();
key.insert("title".to_string(), ParseValue::Number(1.0));
indexes.insert("title_1".to_string(), ParseValue::Object(key));
schema.indexes = Some(indexes);
let rendered = render(&schema);
assert_eq!(rendered["indexes"]["title_1"]["title"], json!(1));
}
#[test]
fn a_parametric_type_renders_its_target_class_as_a_separate_key() {
let schema = ClassSchema::new("_Role").with_field(
"users",
FieldType::Relation {
target_class: "_User".into(),
},
);
assert_eq!(
render(&schema)["fields"]["users"],
json!({ "type": "Relation", "targetClass": "_User" }),
"not the `relation<_User>` storage spelling"
);
}
}