Skip to main content

parse_rust_rest/
pipeline.rs

1//! The read and write pipelines.
2//!
3//! Generic over [`StorageAdapter`] rather than taking a `dyn`, so the storage boundary costs
4//! nothing at runtime and a future Postgres adapter drops in by type rather than by trait object.
5//!
6//! Schema handling here is deliberately simple for 0.1.0: every call loads all schemas from the
7//! adapter. Upstream caches instead, and caching is deferred here rather than skipped by
8//! accident. When it lands, the staleness window is a decision to make deliberately and not a
9//! tuning knob: a schema read from cache decides what a write may contain and what a caller may
10//! see, so a tightened rule that has not propagated yet is still being enforced in its old,
11//! looser form. Correctness and authorization are the same question here.
12
13use parse_rust_core::{new_object_id, ErrorCode, ParseDate, ParseError, ParseMap, ParseValue};
14use parse_rust_schema::{apply, default_schema, validate_write};
15use parse_rust_storage::{ClassSchema, Constraint, QueryOptions, StorageAdapter};
16
17use crate::acl::{lower_acl, raise_acl, AclScope};
18
19/// What a create returns to the client: `{objectId, createdAt}` and nothing else.
20#[derive(Debug, Clone)]
21pub struct CreateResponse {
22    pub object_id: String,
23    pub created_at: ParseDate,
24}
25
26/// What an update returns: `{updatedAt}`.
27#[derive(Debug, Clone)]
28pub struct UpdateResponse {
29    pub updated_at: ParseDate,
30}
31
32/// Load the schema for a class, or the default if the class does not exist yet.
33async fn load_schema<S: StorageAdapter>(
34    storage: &S,
35    class_name: &str,
36) -> Result<ClassSchema, ParseError> {
37    let all = storage.all_schemas().await?;
38    Ok(all
39        .into_iter()
40        .find(|s| s.class_name == class_name)
41        .unwrap_or_else(|| default_schema(class_name)))
42}
43
44/// Create an object.
45///
46/// Order matters and is upstream's: validate the schema *before* writing, persist the schema
47/// change only after the row commits. A schema applied before a failed write leaves a phantom
48/// column that no later write can remove.
49pub async fn create<S: StorageAdapter>(
50    storage: &S,
51    class_name: &str,
52    body: ParseMap,
53    scope: &AclScope,
54) -> Result<CreateResponse, ParseError> {
55    let mut schema = load_schema(storage, class_name).await?;
56
57    // Signup pre-generates an objectId so it can build the user's private ACL before the write.
58    // Honour one if it is already present rather than overwriting it, which would leave the ACL
59    // pointing at an id the row does not have.
60    let object_id = match body.get("objectId") {
61        Some(ParseValue::String(id)) => id.clone(),
62        _ => new_object_id(),
63    };
64    let now = ParseDate::now();
65
66    let mut row = body;
67    row.insert(
68        "objectId".to_string(),
69        ParseValue::String(object_id.clone()),
70    );
71    row.insert("createdAt".to_string(), ParseValue::Date(now));
72    row.insert("updatedAt".to_string(), ParseValue::Date(now));
73
74    let delta = validate_write(&schema, &row)?;
75
76    // ACL is split into columns after validation, because `_rperm` and `_wperm` are not fields and
77    // would otherwise be validated as though a client had named them.
78    let stored = lower_acl(row);
79
80    apply(&mut schema, &delta);
81    storage.create(&schema, &stored).await?;
82    if !delta.is_empty() {
83        storage.upsert_schema(&schema).await?;
84    }
85
86    let _ = scope; // ACL does not gate creation; CLP would, and is out of scope for 0.1.0.
87    Ok(CreateResponse {
88        object_id,
89        created_at: now,
90    })
91}
92
93/// Find objects.
94pub async fn find<S: StorageAdapter>(
95    storage: &S,
96    class_name: &str,
97    mut constraints: Vec<Constraint>,
98    options: QueryOptions,
99    scope: &AclScope,
100) -> Result<Vec<ParseMap>, ParseError> {
101    let schema = load_schema(storage, class_name).await?;
102    if let Some(acl) = scope.read_constraint() {
103        constraints.push(acl);
104    }
105    let rows = storage.find(&schema, &constraints, &options).await?;
106    Ok(rows.into_iter().map(raise_acl).collect())
107}
108
109/// Fetch one object by id.
110///
111/// A row the caller cannot read is `OBJECT_NOT_FOUND`, the same as one that does not exist.
112/// Upstream conflates them deliberately: distinguishing them would tell an unauthorized caller
113/// that the object exists.
114pub async fn get<S: StorageAdapter>(
115    storage: &S,
116    class_name: &str,
117    object_id: &str,
118    scope: &AclScope,
119) -> Result<ParseMap, ParseError> {
120    let rows = find(
121        storage,
122        class_name,
123        vec![Constraint::equal(
124            "objectId",
125            ParseValue::String(object_id.to_string()),
126        )],
127        QueryOptions {
128            limit: Some(1),
129            ..Default::default()
130        },
131        scope,
132    )
133    .await?;
134
135    rows.into_iter().next().ok_or_else(object_not_found)
136}
137
138pub async fn count<S: StorageAdapter>(
139    storage: &S,
140    class_name: &str,
141    mut constraints: Vec<Constraint>,
142    scope: &AclScope,
143) -> Result<u64, ParseError> {
144    let schema = load_schema(storage, class_name).await?;
145    if let Some(acl) = scope.read_constraint() {
146        constraints.push(acl);
147    }
148    storage.count(&schema, &constraints).await
149}
150
151/// Update one object by id.
152pub async fn update<S: StorageAdapter>(
153    storage: &S,
154    class_name: &str,
155    object_id: &str,
156    body: ParseMap,
157    scope: &AclScope,
158) -> Result<UpdateResponse, ParseError> {
159    let mut schema = load_schema(storage, class_name).await?;
160
161    let now = ParseDate::now();
162    let mut row = body;
163    // A client cannot move an object or rewrite its creation time.
164    row.shift_remove("objectId");
165    row.shift_remove("createdAt");
166    row.insert("updatedAt".to_string(), ParseValue::Date(now));
167
168    let delta = validate_write(&schema, &row)?;
169    let values = lower_acl(row);
170
171    let mut constraints = vec![Constraint::equal(
172        "objectId",
173        ParseValue::String(object_id.to_string()),
174    )];
175    if let Some(acl) = scope.write_constraint() {
176        constraints.push(acl);
177    }
178
179    apply(&mut schema, &delta);
180    let matched = storage.update(&schema, &constraints, &values).await?;
181    if matched == 0 {
182        return Err(object_not_found());
183    }
184    if !delta.is_empty() {
185        storage.upsert_schema(&schema).await?;
186    }
187
188    Ok(UpdateResponse { updated_at: now })
189}
190
191/// Delete one object by id.
192pub async fn delete<S: StorageAdapter>(
193    storage: &S,
194    class_name: &str,
195    object_id: &str,
196    scope: &AclScope,
197) -> Result<(), ParseError> {
198    let schema = load_schema(storage, class_name).await?;
199    let mut constraints = vec![Constraint::equal(
200        "objectId",
201        ParseValue::String(object_id.to_string()),
202    )];
203    if let Some(acl) = scope.write_constraint() {
204        constraints.push(acl);
205    }
206    let deleted = storage.delete(&schema, &constraints).await?;
207    if deleted == 0 {
208        return Err(object_not_found());
209    }
210    Ok(())
211}
212
213/// The error both "does not exist" and "you cannot see it" produce.
214fn object_not_found() -> ParseError {
215    ParseError::new(ErrorCode::ObjectNotFound, "Object not found.")
216}