parse-rust-storage 0.1.0

StorageAdapter trait and the query/update AST adapters lower.
Documentation
//! The `StorageAdapter` trait.
//!
//! **Shaped by two consumers, not one.** Building against Mongo alone bakes Mongo-isms into the
//! interface and the Postgres port then fights it, which is what produced the 55 catalogued
//! divergences upstream. The rule: if a method can only be implemented sensibly
//! for one backend, the trait is wrong.
//!
//! Two consequences visible in the signatures below:
//!
//! - **No `$` operators and no BSON.** Queries are an AST the adapter lowers. Postgres cannot
//!   lower a raw Mongo query document, so accepting one here would be the first Mongo-ism.
//! - **Schema is passed in, not fetched.** The adapter does not own a schema cache. A caller that
//!   already resolved the schema for a request threads it down, which is what keeps one request
//!   from evaluating half its work under two different schemas.

use std::future::Future;

use parse_rust_core::{ParseError, ParseMap};

use crate::query::{Constraint, QueryOptions};
use crate::schema::ClassSchema;

/// A row as stored: Parse-format values, no backend encoding.
pub type Row = ParseMap;

/// What a write returns.
///
/// Deliberately not the full row. Upstream's create response is `{objectId, createdAt}` and its
/// update response is `{updatedAt}`, and returning more here would tempt a caller into sending
/// more than parse-server does.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct WriteResult {
    pub object_id: String,
}

/// Storage operations.
///
/// `async fn` in trait, so this is not object-safe. That is deliberate for now: the server holds
/// one concrete adapter chosen at construction, and boxing every call to support a `dyn` we do
/// not need would cost allocations on the hot path. If a deployment ever needs to swap adapters
/// at runtime, add a boxed wrapper rather than degrading this.
pub trait StorageAdapter: Send + Sync {
    /// Load every class schema. Upstream has no per-class fetch: a miss on any class triggers a
    /// full `getAllClasses`, and reproducing that shape keeps the caching behavior comparable.
    fn all_schemas(&self) -> impl Future<Output = Result<Vec<ClassSchema>, ParseError>> + Send;

    /// Persist a class schema, creating the class if it does not exist.
    fn upsert_schema(
        &self,
        schema: &ClassSchema,
    ) -> impl Future<Output = Result<(), ParseError>> + Send;

    /// Insert one row. `object_id` is generated by the caller, not the adapter, because it is
    /// part of the Parse contract rather than a storage detail.
    fn create(
        &self,
        schema: &ClassSchema,
        row: &Row,
    ) -> impl Future<Output = Result<WriteResult, ParseError>> + Send;

    /// Find rows matching every constraint.
    fn find(
        &self,
        schema: &ClassSchema,
        constraints: &[Constraint],
        options: &QueryOptions,
    ) -> impl Future<Output = Result<Vec<Row>, ParseError>> + Send;

    /// Count rows matching every constraint.
    fn count(
        &self,
        schema: &ClassSchema,
        constraints: &[Constraint],
    ) -> impl Future<Output = Result<u64, ParseError>> + Send;

    /// Update matching rows with the given field values.
    ///
    /// Returns how many rows matched, so a caller can distinguish "updated nothing because the
    /// object does not exist" from "updated nothing because the ACL excluded it". Upstream
    /// conflates those into `OBJECT_NOT_FOUND`, which is the behavior to reproduce at the REST
    /// layer, but the adapter should not throw the information away before then.
    fn update(
        &self,
        schema: &ClassSchema,
        constraints: &[Constraint],
        values: &Row,
    ) -> impl Future<Output = Result<u64, ParseError>> + Send;

    /// Delete matching rows. Returns how many, for the same reason as `update`.
    fn delete(
        &self,
        schema: &ClassSchema,
        constraints: &[Constraint],
    ) -> impl Future<Output = Result<u64, ParseError>> + Send;

    /// Create a unique index.
    ///
    /// **Index names are part of the contract.** Both adapters recover `duplicated_field` by regex
    /// over the index name, and the Mongo regex matches only auto-generated `<field>_1` names, so
    /// a differently-named index changes the error a client sees. `name: None` means "let the
    /// backend auto-name it", which is what produces `username_1`.
    fn ensure_unique_index(
        &self,
        class_name: &str,
        fields: &[&str],
        name: Option<&str>,
    ) -> impl Future<Output = Result<(), ParseError>> + Send;
}