use parse_rust_core::{js_number, ErrorCode, ErrorDetail, ParseError, ParseMap, ParseValue};
use parse_rust_storage::{
join_schema, Clause, Comparison, Constraint, Query, QueryOptions, StorageAdapter,
};
#[derive(Debug, Clone)]
pub struct RelationUpdate {
pub key: String,
pub kind: RelationOpKind,
pub related_ids: Vec<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RelationOpKind {
Add,
Remove,
}
#[derive(Debug, Clone)]
pub enum RelatedToOutcome {
Ids(Vec<String>),
DeniedYieldEmpty,
}
impl RelatedToOutcome {
pub fn ids(&self) -> &[String] {
match self {
RelatedToOutcome::Ids(ids) => ids,
RelatedToOutcome::DeniedYieldEmpty => &[],
}
}
}
fn join_query_options(keys: &[&str]) -> QueryOptions {
QueryOptions {
limit: None,
skip: None,
order: Vec::new(),
keys: Some(keys.iter().map(|k| k.to_string()).collect()),
case_insensitive: false,
}
}
pub fn collect_relation_updates(body: &mut crate::WriteBody) -> Vec<RelationUpdate> {
use parse_rust_core::{FieldWrite, Op};
fn walk(key: &str, op: &Op, out: &mut Vec<RelationUpdate>) -> bool {
match op {
Op::AddRelation(objects) => {
out.push(RelationUpdate {
key: key.to_string(),
kind: RelationOpKind::Add,
related_ids: related_object_ids(objects),
});
true
}
Op::RemoveRelation(objects) => {
out.push(RelationUpdate {
key: key.to_string(),
kind: RelationOpKind::Remove,
related_ids: related_object_ids(objects),
});
true
}
Op::Batch(ops) => {
let mut any = false;
for inner in ops {
any |= walk(key, inner, out);
}
any
}
_ => false,
}
}
let mut updates = Vec::new();
let mut remove: Vec<String> = Vec::new();
for (key, write) in body.iter() {
if let FieldWrite::Op(op) = write {
if walk(key, op, &mut updates) {
remove.push(key.clone());
}
}
}
for key in remove {
body.shift_remove(&key);
}
updates
}
fn related_object_ids(objects: &[ParseValue]) -> Vec<String> {
objects
.iter()
.filter_map(|v| match v {
ParseValue::Pointer { object_id, .. } => Some(object_id.clone()),
ParseValue::Object(map) => match map.get("objectId") {
Some(ParseValue::String(id)) => Some(id.clone()),
_ => None,
},
_ => None,
})
.collect()
}
pub async fn apply_relation_updates<S: StorageAdapter>(
storage: &S,
class_name: &str,
object_id: &str,
updates: &[RelationUpdate],
) -> Result<(), ParseError> {
for update in updates {
let schema = join_schema(class_name, &update.key);
for related_id in &update.related_ids {
let mut doc = ParseMap::new();
doc.insert(
"relatedId".to_string(),
ParseValue::String(related_id.clone()),
);
doc.insert(
"owningId".to_string(),
ParseValue::String(object_id.to_string()),
);
let query = Query::from_constraints(vec![
Constraint::equal("relatedId", ParseValue::String(related_id.clone())),
Constraint::equal("owningId", ParseValue::String(object_id.to_string())),
]);
match update.kind {
RelationOpKind::Add => storage.upsert_one(&schema, &query, &doc).await?,
RelationOpKind::Remove => match storage.delete(&schema, &query).await {
Ok(_) => {}
Err(e) if e.code == ErrorCode::ObjectNotFound => {}
Err(e) => return Err(e),
},
}
}
}
Ok(())
}
pub async fn related_ids<S: StorageAdapter>(
storage: &S,
owning_class: &str,
key: &str,
owning_id: &str,
) -> Result<Vec<String>, ParseError> {
let schema = join_schema(owning_class, key);
let query = Query::from_constraints(vec![Constraint::equal(
"owningId",
ParseValue::String(owning_id.to_string()),
)]);
let rows = storage
.find(&schema, &query, &join_query_options(&["relatedId"]))
.await?;
Ok(string_column(rows, "relatedId"))
}
pub async fn owning_ids<S: StorageAdapter>(
storage: &S,
owning_class: &str,
key: &str,
related_ids: &[String],
) -> Result<Vec<String>, ParseError> {
let schema = join_schema(owning_class, key);
let query = Query::from_constraints(vec![Constraint::one_of(
"relatedId",
related_ids
.iter()
.map(|id| ParseValue::String(id.clone()))
.collect(),
)]);
let rows = storage
.find(&schema, &query, &join_query_options(&["owningId"]))
.await?;
Ok(string_column(rows, "owningId"))
}
fn string_column(rows: Vec<ParseMap>, key: &str) -> Vec<String> {
rows.into_iter()
.filter_map(|row| match row.get(key) {
Some(ParseValue::String(s)) => Some(s.clone()),
_ => None,
})
.collect()
}
pub async fn authorize_related_to<F, Fut>(
owning_class: &str,
relation_key: &str,
owning_protected_fields: &[String],
detail: ErrorDetail,
can_read_owning: F,
) -> Result<bool, ParseError>
where
F: FnOnce() -> Fut,
Fut: std::future::Future<Output = Result<bool, ParseError>>,
{
let root = relation_key.split('.').next().unwrap_or(relation_key);
if owning_protected_fields
.iter()
.any(|f| f == relation_key || f == root)
{
return Err(ParseError::permission_denied(
ErrorCode::OperationForbidden,
format!("This user is not allowed to query {relation_key} on class {owning_class}"),
detail,
));
}
can_read_owning().await
}
#[derive(Debug, Clone)]
pub enum RelationConstraint {
OwnersOf(Vec<String>),
NotOwnersOf(Vec<String>),
}
fn object_id_of(value: &ParseValue) -> Result<Option<String>, ParseError> {
match value {
ParseValue::Null => Err(ParseError::invalid_json(
"cannot use null in a constraint on a Relation field",
)),
ParseValue::Pointer { object_id, .. } => Ok(Some(object_id.clone())),
ParseValue::Object(map) => Ok(match map.get("objectId") {
Some(ParseValue::String(id)) => Some(id.clone()),
_ => None,
}),
_ => Ok(None),
}
}
fn is_tagged_pointer(value: &ParseValue) -> bool {
match value {
ParseValue::Pointer { .. } => true,
ParseValue::Object(map) => {
matches!(map.get("__type"), Some(ParseValue::String(t)) if t == "Pointer")
}
_ => false,
}
}
fn satisfies_gate(comparison: &Comparison) -> bool {
match comparison {
Comparison::Equal(v) => is_tagged_pointer(v),
Comparison::In(_) | Comparison::NotIn(_) => true,
Comparison::NotEqual(v) => js_number::is_truthy(v),
_ => false,
}
}
pub fn relation_constraints_for(
comparisons: &[Comparison],
) -> Result<Vec<RelationConstraint>, ParseError> {
fn ids(values: &[ParseValue]) -> Result<Vec<String>, ParseError> {
let mut out = Vec::new();
for value in values {
if let Some(id) = object_id_of(value)? {
out.push(id);
}
}
Ok(out)
}
if !comparisons.iter().any(satisfies_gate) {
return Ok(vec![RelationConstraint::OwnersOf(Vec::new())]);
}
let mut out = Vec::new();
for comparison in comparisons {
out.push(match comparison {
Comparison::Equal(v) if is_tagged_pointer(v) => {
RelationConstraint::OwnersOf(object_id_of(v)?.into_iter().collect())
}
Comparison::In(values) => RelationConstraint::OwnersOf(ids(values)?),
Comparison::NotIn(values) => RelationConstraint::NotOwnersOf(ids(values)?),
Comparison::NotEqual(v) => {
RelationConstraint::NotOwnersOf(object_id_of(v)?.into_iter().collect())
}
_ => continue,
});
}
Ok(out)
}
pub fn add_in_object_ids(query: &mut Query, ids: &[String]) {
let mut sets: Vec<Vec<String>> = Vec::new();
query.clauses.retain(|clause| match clause {
Clause::Field(Constraint { field, comparison }) if field == "objectId" => {
match comparison {
Comparison::Equal(ParseValue::String(id)) => {
sets.push(vec![id.clone()]);
false
}
Comparison::In(values) => {
sets.push(
values
.iter()
.filter_map(|v| match v {
ParseValue::String(s) => Some(s.clone()),
_ => None,
})
.collect(),
);
false
}
_ => true,
}
}
_ => true,
});
sets.push(ids.to_vec());
let mut intersection: Vec<String> = Vec::new();
if let Some((first, rest)) = sets.split_first() {
for id in first {
if !intersection.contains(id) && rest.iter().all(|set| set.contains(id)) {
intersection.push(id.clone());
}
}
}
query.push_constraint(Constraint::one_of(
"objectId",
intersection.into_iter().map(ParseValue::String).collect(),
));
}
pub fn add_not_in_object_ids(query: &mut Query, ids: &[String]) {
let mut union: Vec<String> = Vec::new();
query.clauses.retain(|clause| match clause {
Clause::Field(Constraint {
field,
comparison: Comparison::NotIn(values),
}) if field == "objectId" => {
for v in values {
if let ParseValue::String(s) = v {
if !union.contains(s) {
union.push(s.clone());
}
}
}
false
}
_ => true,
});
for id in ids {
if !union.contains(id) {
union.push(id.clone());
}
}
query.push_constraint(Constraint {
field: "objectId".to_string(),
comparison: Comparison::NotIn(union.into_iter().map(ParseValue::String).collect()),
});
}
#[cfg(test)]
mod tests {
use super::*;
use parse_rust_core::{op::OpPath, FieldWrite};
fn body(json: &str) -> crate::WriteBody {
crate::decode_write_body(
&serde_json::from_str(json).expect("test literal"),
OpPath::Update,
)
.expect("decode")
}
#[test]
fn relation_ops_are_stripped_out_of_the_write() {
let mut b = body(
r#"{
"name":"admins",
"users":{"__op":"AddRelation","objects":[
{"__type":"Pointer","className":"_User","objectId":"u1"},
{"__type":"Pointer","className":"_User","objectId":"u2"}
]}
}"#,
);
let ops = collect_relation_updates(&mut b);
assert!(b.contains_key("name"));
assert!(
!b.contains_key("users"),
"a Relation field has no column, so it must not reach the row write"
);
assert_eq!(ops.len(), 1);
assert_eq!(ops[0].kind, RelationOpKind::Add);
assert_eq!(ops[0].related_ids, vec!["u1", "u2"]);
}
#[test]
fn a_batch_of_relation_ops_is_stripped_whole() {
let mut b = body(
r#"{"users":{"__op":"Batch","ops":[
{"__op":"AddRelation","objects":[{"__type":"Pointer","className":"_User","objectId":"u1"}]},
{"__op":"RemoveRelation","objects":[{"__type":"Pointer","className":"_User","objectId":"u2"}]}
]}}"#,
);
let ops = collect_relation_updates(&mut b);
assert!(b.is_empty());
assert_eq!(ops.len(), 2);
assert_eq!(ops[0].kind, RelationOpKind::Add);
assert_eq!(ops[1].kind, RelationOpKind::Remove);
}
#[test]
fn a_non_relation_op_is_left_alone() {
let mut b = body(r#"{"views":{"__op":"Increment","amount":1}}"#);
assert!(collect_relation_updates(&mut b).is_empty());
assert!(matches!(b.get("views"), Some(FieldWrite::Op(_))));
}
#[test]
fn relation_field_constraints_map_to_the_reverse_join() {
let decoded = ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
};
let mut raw = ParseMap::new();
raw.insert("__type".into(), ParseValue::String("Pointer".into()));
raw.insert("className".into(), ParseValue::String("_User".into()));
raw.insert("objectId".into(), ParseValue::String("u1".into()));
raw.insert("extra".into(), ParseValue::Number(7.0));
let raw = ParseValue::Object(raw);
let one = |c: Comparison| {
let out = relation_constraints_for(&[c]).expect("accepted");
assert_eq!(out.len(), 1, "{out:?}");
out.into_iter().next().unwrap()
};
for pointer in [decoded, raw] {
assert!(matches!(
one(Comparison::Equal(pointer.clone())),
RelationConstraint::OwnersOf(ids) if ids == ["u1"]
));
assert!(matches!(
one(Comparison::In(vec![pointer.clone()])),
RelationConstraint::OwnersOf(ids) if ids == ["u1"]
));
assert!(matches!(
one(Comparison::NotIn(vec![pointer.clone()])),
RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
));
assert!(matches!(
one(Comparison::NotEqual(pointer)),
RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
));
}
assert!(matches!(
one(Comparison::Exists(true)),
RelationConstraint::OwnersOf(ids) if ids.is_empty()
));
let untagged = {
let mut m = ParseMap::new();
m.insert("objectId".into(), ParseValue::String("u1".into()));
ParseValue::Object(m)
};
assert!(matches!(
one(Comparison::Equal(untagged.clone())),
RelationConstraint::OwnersOf(ids) if ids.is_empty()
));
assert!(matches!(
one(Comparison::NotIn(vec![untagged.clone()])),
RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
));
assert!(matches!(
one(Comparison::In(vec![untagged.clone()])),
RelationConstraint::OwnersOf(ids) if ids == ["u1"]
));
assert!(matches!(
one(Comparison::NotEqual(untagged)),
RelationConstraint::NotOwnersOf(ids) if ids == ["u1"]
));
}
#[test]
fn a_falsy_ne_fails_the_gate_and_returns_no_owners() {
for falsy in [
ParseValue::Null,
ParseValue::Bool(false),
ParseValue::Number(0.0),
ParseValue::String(String::new()),
] {
let out =
relation_constraints_for(&[Comparison::NotEqual(falsy.clone())]).expect("accepted");
assert!(
matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
"{falsy:?} must fail the gate, got {out:?}"
);
}
let out = relation_constraints_for(&[Comparison::NotEqual(ParseValue::Number(7.0))])
.expect("accepted");
assert!(
matches!(out.as_slice(), [RelationConstraint::NotOwnersOf(ids)] if ids.is_empty()),
"{out:?}"
);
}
#[test]
fn a_null_operand_is_refused_rather_than_erased() {
for comparison in [
Comparison::In(vec![ParseValue::Null]),
Comparison::NotIn(vec![ParseValue::Null]),
Comparison::In(vec![
ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
},
ParseValue::Null,
]),
] {
let err = relation_constraints_for(std::slice::from_ref(&comparison))
.expect_err("a null operand is refused");
assert_eq!(
err.message, "cannot use null in a constraint on a Relation field",
"{comparison:?}"
);
}
let err = relation_constraints_for(&[
Comparison::NotEqual(ParseValue::Null),
Comparison::In(Vec::new()),
])
.expect_err("refused");
assert_eq!(
err.message,
"cannot use null in a constraint on a Relation field"
);
let out = relation_constraints_for(&[Comparison::NotEqual(ParseValue::Null)])
.expect("the gate fails before anything is read");
assert!(
matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
"{out:?}"
);
let out = relation_constraints_for(&[Comparison::In(vec![ParseValue::Number(7.0)])])
.expect("accepted");
assert!(
matches!(out.as_slice(), [RelationConstraint::OwnersOf(ids)] if ids.is_empty()),
"{out:?}"
);
}
#[test]
fn a_truthy_sibling_carries_a_falsy_ne_through_the_gate() {
let pointer = ParseValue::Pointer {
class_name: "_User".into(),
object_id: "u1".into(),
};
let out = relation_constraints_for(&[
Comparison::NotEqual(ParseValue::Bool(false)),
Comparison::In(vec![pointer]),
])
.expect("accepted");
assert!(
matches!(
out.as_slice(),
[RelationConstraint::NotOwnersOf(none), RelationConstraint::OwnersOf(one)]
if none.is_empty() && one.as_slice() == ["u1"]
),
"{out:?}"
);
}
#[test]
fn object_id_constraints_intersect_rather_than_stack() {
let mut q = Query::from_constraints(vec![Constraint::equal(
"objectId",
ParseValue::String("a".into()),
)]);
add_in_object_ids(&mut q, &["a".to_string(), "b".to_string()]);
assert_eq!(q.clauses.len(), 1, "the original constraint is folded in");
match &q.clauses[0] {
Clause::Field(Constraint {
comparison: Comparison::In(values),
..
}) => assert_eq!(values.len(), 1),
other => panic!("expected an In, got {other:?}"),
}
}
#[test]
fn a_denied_related_to_intersects_to_nothing() {
let mut q = Query::new();
add_in_object_ids(&mut q, &[]);
match &q.clauses[0] {
Clause::Field(Constraint {
comparison: Comparison::In(values),
..
}) => assert!(values.is_empty()),
other => panic!("expected an empty In, got {other:?}"),
}
}
#[test]
fn not_in_object_ids_unions() {
let mut q = Query::from_constraints(vec![Constraint {
field: "objectId".into(),
comparison: Comparison::NotIn(vec![ParseValue::String("a".into())]),
}]);
add_not_in_object_ids(&mut q, &["b".to_string(), "a".to_string()]);
assert_eq!(q.clauses.len(), 1);
match &q.clauses[0] {
Clause::Field(Constraint {
comparison: Comparison::NotIn(values),
..
}) => assert_eq!(values.len(), 2),
other => panic!("expected a NotIn, got {other:?}"),
}
}
#[tokio::test]
async fn the_protected_key_check_precedes_the_read() {
let e = authorize_related_to(
"_Role",
"users",
&["users".to_string()],
ErrorDetail::Disclosed,
|| async { panic!("the owning object must not be read once the key is refused") },
)
.await
.unwrap_err();
assert_eq!(e.code, ErrorCode::OperationForbidden);
assert_eq!(
e.message,
"This user is not allowed to query users on class _Role"
);
let withheld = authorize_related_to(
"_Role",
"users",
&["users".to_string()],
ErrorDetail::Withheld,
|| async { panic!("the owning object must not be read once the key is refused") },
)
.await
.unwrap_err();
assert_eq!(withheld.code, ErrorCode::OperationForbidden);
assert_eq!(withheld.message, "Permission denied");
}
}