use serde_json::{json, Map, Value};
pub use syncular_runtime::app_schema::{
AppTableMetadata, ColumnMetadata, CrdtYjsFieldMetadata, EncryptedFieldMetadata, ScopeMetadata,
ScopeSource,
};
#[allow(unused_imports)]
use syncular_runtime::client::{
SubscriptionSpec, SyncChangedRow, SyncularClientConfig, SyncularCommandHistoryExecutor,
SyncularEncryptedCrdtMutationExecutor, SyncularLeasedMutationExecutor,
SyncularMutationExecutor,
};
use syncular_runtime::command_history::{
CommandHistoryEntry, CommandHistoryReceipt, CommandHistoryRecord, CommandHistoryState,
};
use syncular_runtime::crdt_yjs::{YjsUpdateEnvelope, YJS_PAYLOAD_KEY};
use syncular_runtime::encryption::FieldEncryptionRule;
use syncular_runtime::error::{Result, SyncularError};
use syncular_runtime::protocol::{
random_syncular_id, IntoSyncularMutation, MutationCommit, MutationReceipt,
PendingSyncularMutation, SyncOperation, SyncularMutationBatch, SyncularMutationKind,
};
pub const APP_TABLES: &[&str] = &["comments", "projects", "tasks"];
pub const COMMENTS_TABLE: &str = "comments";
pub const PROJECTS_TABLE: &str = "projects";
pub const TASKS_TABLE: &str = "tasks";
pub const COMMENTS_COLUMNS: &[ColumnMetadata] = &[
ColumnMetadata {
name: "id",
type_family: "text",
notnull_required: false,
primary_key: true,
},
ColumnMetadata {
name: "task_id",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "project_id",
type_family: "text",
notnull_required: false,
primary_key: false,
},
ColumnMetadata {
name: "body",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "author_id",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "deleted",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "server_version",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
];
pub const COMMENTS_BLOB_COLUMNS: &[&str] = &[];
pub const COMMENTS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[];
pub const COMMENTS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[];
pub const COMMENTS_SCOPES: &[ScopeMetadata] = &[
ScopeMetadata {
name: "user_id",
column: "author_id",
source: ScopeSource::ActorId,
required: true,
},
ScopeMetadata {
name: "project_id",
column: "project_id",
source: ScopeSource::ProjectId,
required: false,
},
];
pub const COMMENTS_METADATA: AppTableMetadata = AppTableMetadata {
name: "comments",
primary_key_column: "id",
server_version_column: "server_version",
soft_delete_column: Some("deleted"),
subscription_id: "sub-comments",
columns: COMMENTS_COLUMNS,
blob_columns: COMMENTS_BLOB_COLUMNS,
crdt_yjs_fields: COMMENTS_CRDT_YJS_FIELDS,
encrypted_fields: COMMENTS_ENCRYPTED_FIELDS,
scopes: COMMENTS_SCOPES,
};
pub const PROJECTS_COLUMNS: &[ColumnMetadata] = &[
ColumnMetadata {
name: "id",
type_family: "text",
notnull_required: false,
primary_key: true,
},
ColumnMetadata {
name: "name",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "owner_id",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "archived",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "server_version",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
];
pub const PROJECTS_BLOB_COLUMNS: &[&str] = &[];
pub const PROJECTS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[];
pub const PROJECTS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[];
pub const PROJECTS_SCOPES: &[ScopeMetadata] = &[ScopeMetadata {
name: "user_id",
column: "owner_id",
source: ScopeSource::ActorId,
required: true,
}];
pub const PROJECTS_METADATA: AppTableMetadata = AppTableMetadata {
name: "projects",
primary_key_column: "id",
server_version_column: "server_version",
soft_delete_column: None,
subscription_id: "sub-projects",
columns: PROJECTS_COLUMNS,
blob_columns: PROJECTS_BLOB_COLUMNS,
crdt_yjs_fields: PROJECTS_CRDT_YJS_FIELDS,
encrypted_fields: PROJECTS_ENCRYPTED_FIELDS,
scopes: PROJECTS_SCOPES,
};
pub const TASKS_COLUMNS: &[ColumnMetadata] = &[
ColumnMetadata {
name: "id",
type_family: "text",
notnull_required: false,
primary_key: true,
},
ColumnMetadata {
name: "title",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "completed",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "user_id",
type_family: "text",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "project_id",
type_family: "text",
notnull_required: false,
primary_key: false,
},
ColumnMetadata {
name: "server_version",
type_family: "integer",
notnull_required: true,
primary_key: false,
},
ColumnMetadata {
name: "image",
type_family: "text",
notnull_required: false,
primary_key: false,
},
ColumnMetadata {
name: "title_yjs_state",
type_family: "text",
notnull_required: false,
primary_key: false,
},
ColumnMetadata {
name: "description",
type_family: "text",
notnull_required: false,
primary_key: false,
},
];
pub const TASKS_BLOB_COLUMNS: &[&str] = &["image"];
pub const TASKS_CRDT_YJS_FIELDS: &[CrdtYjsFieldMetadata] = &[CrdtYjsFieldMetadata {
field: "title",
state_column: "title_yjs_state",
container_key: "title",
row_id_field: "id",
kind: "text",
sync_mode: "server-merge",
}];
pub const TASKS_ENCRYPTED_FIELDS: &[EncryptedFieldMetadata] = &[EncryptedFieldMetadata {
field: "description",
scope: "tasks",
row_id_field: "id",
}];
pub const TASKS_SCOPES: &[ScopeMetadata] = &[
ScopeMetadata {
name: "user_id",
column: "user_id",
source: ScopeSource::ActorId,
required: true,
},
ScopeMetadata {
name: "project_id",
column: "project_id",
source: ScopeSource::ProjectId,
required: false,
},
];
pub const TASKS_METADATA: AppTableMetadata = AppTableMetadata {
name: "tasks",
primary_key_column: "id",
server_version_column: "server_version",
soft_delete_column: None,
subscription_id: "sub-tasks",
columns: TASKS_COLUMNS,
blob_columns: TASKS_BLOB_COLUMNS,
crdt_yjs_fields: TASKS_CRDT_YJS_FIELDS,
encrypted_fields: TASKS_ENCRYPTED_FIELDS,
scopes: TASKS_SCOPES,
};
pub const APP_TABLE_METADATA: &[AppTableMetadata] =
&[COMMENTS_METADATA, PROJECTS_METADATA, TASKS_METADATA];
pub fn table_metadata(table: &str) -> Option<&'static AppTableMetadata> {
APP_TABLE_METADATA
.iter()
.find(|metadata| metadata.name == table)
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct CommentChangedFields {
pub id: bool,
pub task_id: bool,
pub project_id: bool,
pub body: bool,
pub author_id: bool,
pub deleted: bool,
pub server_version: bool,
}
impl CommentChangedFields {
pub fn from_columns(columns: &[String]) -> Self {
Self {
id: columns.iter().any(|column| column == "id"),
task_id: columns.iter().any(|column| column == "task_id"),
project_id: columns.iter().any(|column| column == "project_id"),
body: columns.iter().any(|column| column == "body"),
author_id: columns.iter().any(|column| column == "author_id"),
deleted: columns.iter().any(|column| column == "deleted"),
server_version: columns.iter().any(|column| column == "server_version"),
}
}
pub fn contains(&self, column: &str) -> bool {
match column {
"id" => self.id,
"task_id" => self.task_id,
"project_id" => self.project_id,
"body" => self.body,
"author_id" => self.author_id,
"deleted" => self.deleted,
"server_version" => self.server_version,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct CommentChangedRow<'a> {
pub raw: &'a SyncChangedRow,
pub changed: CommentChangedFields,
pub crdt: CommentChangedFields,
}
impl<'a> CommentChangedRow<'a> {
pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
if row.table != "comments" {
return None;
}
Some(Self {
raw: row,
changed: CommentChangedFields::from_columns(&row.changed_fields),
crdt: CommentChangedFields::from_columns(&row.crdt_fields),
})
}
pub fn row_id(&self) -> Option<&str> {
self.raw.row_id.as_deref()
}
pub fn is_insert(&self) -> bool {
self.raw.operation == "insert"
}
pub fn is_update(&self) -> bool {
self.raw.operation == "update"
}
pub fn is_delete(&self) -> bool {
self.raw.operation == "delete"
}
}
pub fn comment_changed_rows<'a>(
rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<CommentChangedRow<'a>> {
rows.into_iter()
.filter_map(CommentChangedRow::from_raw)
.collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct ProjectChangedFields {
pub id: bool,
pub name: bool,
pub owner_id: bool,
pub archived: bool,
pub server_version: bool,
}
impl ProjectChangedFields {
pub fn from_columns(columns: &[String]) -> Self {
Self {
id: columns.iter().any(|column| column == "id"),
name: columns.iter().any(|column| column == "name"),
owner_id: columns.iter().any(|column| column == "owner_id"),
archived: columns.iter().any(|column| column == "archived"),
server_version: columns.iter().any(|column| column == "server_version"),
}
}
pub fn contains(&self, column: &str) -> bool {
match column {
"id" => self.id,
"name" => self.name,
"owner_id" => self.owner_id,
"archived" => self.archived,
"server_version" => self.server_version,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProjectChangedRow<'a> {
pub raw: &'a SyncChangedRow,
pub changed: ProjectChangedFields,
pub crdt: ProjectChangedFields,
}
impl<'a> ProjectChangedRow<'a> {
pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
if row.table != "projects" {
return None;
}
Some(Self {
raw: row,
changed: ProjectChangedFields::from_columns(&row.changed_fields),
crdt: ProjectChangedFields::from_columns(&row.crdt_fields),
})
}
pub fn row_id(&self) -> Option<&str> {
self.raw.row_id.as_deref()
}
pub fn is_insert(&self) -> bool {
self.raw.operation == "insert"
}
pub fn is_update(&self) -> bool {
self.raw.operation == "update"
}
pub fn is_delete(&self) -> bool {
self.raw.operation == "delete"
}
}
pub fn project_changed_rows<'a>(
rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<ProjectChangedRow<'a>> {
rows.into_iter()
.filter_map(ProjectChangedRow::from_raw)
.collect()
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct TaskChangedFields {
pub id: bool,
pub title: bool,
pub completed: bool,
pub user_id: bool,
pub project_id: bool,
pub server_version: bool,
pub image: bool,
pub title_yjs_state: bool,
pub description: bool,
}
impl TaskChangedFields {
pub fn from_columns(columns: &[String]) -> Self {
Self {
id: columns.iter().any(|column| column == "id"),
title: columns.iter().any(|column| column == "title"),
completed: columns.iter().any(|column| column == "completed"),
user_id: columns.iter().any(|column| column == "user_id"),
project_id: columns.iter().any(|column| column == "project_id"),
server_version: columns.iter().any(|column| column == "server_version"),
image: columns.iter().any(|column| column == "image"),
title_yjs_state: columns.iter().any(|column| column == "title_yjs_state"),
description: columns.iter().any(|column| column == "description"),
}
}
pub fn contains(&self, column: &str) -> bool {
match column {
"id" => self.id,
"title" => self.title,
"completed" => self.completed,
"user_id" => self.user_id,
"project_id" => self.project_id,
"server_version" => self.server_version,
"image" => self.image,
"title_yjs_state" => self.title_yjs_state,
"description" => self.description,
_ => false,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TaskChangedRow<'a> {
pub raw: &'a SyncChangedRow,
pub changed: TaskChangedFields,
pub crdt: TaskChangedFields,
}
impl<'a> TaskChangedRow<'a> {
pub fn from_raw(row: &'a SyncChangedRow) -> Option<Self> {
if row.table != "tasks" {
return None;
}
Some(Self {
raw: row,
changed: TaskChangedFields::from_columns(&row.changed_fields),
crdt: TaskChangedFields::from_columns(&row.crdt_fields),
})
}
pub fn row_id(&self) -> Option<&str> {
self.raw.row_id.as_deref()
}
pub fn is_insert(&self) -> bool {
self.raw.operation == "insert"
}
pub fn is_update(&self) -> bool {
self.raw.operation == "update"
}
pub fn is_delete(&self) -> bool {
self.raw.operation == "delete"
}
}
pub fn task_changed_rows<'a>(
rows: impl IntoIterator<Item = &'a SyncChangedRow>,
) -> Vec<TaskChangedRow<'a>> {
rows.into_iter()
.filter_map(TaskChangedRow::from_raw)
.collect()
}
pub fn generated_field_encryption_rules() -> Vec<FieldEncryptionRule> {
vec![FieldEncryptionRule {
scope: "tasks".to_string(),
table: Some("tasks".to_string()),
fields: vec!["description".to_string()],
row_id_field: Some("id".to_string()),
}]
}
pub fn default_subscriptions(config: &SyncularClientConfig) -> Vec<SubscriptionSpec> {
vec![
comments_subscription(&config.actor_id, config.project_id.as_deref()),
projects_subscription(&config.actor_id),
tasks_subscription(&config.actor_id, config.project_id.as_deref()),
]
}
pub fn default_subscriptions_with_bootstrap_phases(
config: &SyncularClientConfig,
bootstrap_phases: &[(&str, i64)],
) -> Vec<SubscriptionSpec> {
let mut subscriptions = default_subscriptions(config);
apply_bootstrap_phases(&mut subscriptions, bootstrap_phases);
subscriptions
}
pub fn apply_bootstrap_phases(
subscriptions: &mut [SubscriptionSpec],
bootstrap_phases: &[(&str, i64)],
) {
for subscription in subscriptions {
if let Some((_, phase)) = bootstrap_phases
.iter()
.find(|(key, _)| *key == subscription.id || *key == subscription.table)
{
subscription.bootstrap_phase = *phase;
}
}
}
pub fn with_bootstrap_phase(
mut subscription: SubscriptionSpec,
bootstrap_phase: i64,
) -> SubscriptionSpec {
subscription.bootstrap_phase = bootstrap_phase;
subscription
}
pub fn comments_subscription(user_id: &str, project_id: Option<&str>) -> SubscriptionSpec {
let mut scopes = Map::new();
scopes.insert("user_id".to_string(), json!(user_id));
if let Some(project_id) = project_id {
scopes.insert("project_id".to_string(), json!(project_id));
}
let params = Map::new();
SubscriptionSpec {
id: "sub-comments".to_string(),
table: "comments".to_string(),
scopes,
params,
bootstrap_phase: 0,
}
}
pub fn projects_subscription(user_id: &str) -> SubscriptionSpec {
let mut scopes = Map::new();
scopes.insert("user_id".to_string(), json!(user_id));
let params = Map::new();
SubscriptionSpec {
id: "sub-projects".to_string(),
table: "projects".to_string(),
scopes,
params,
bootstrap_phase: 0,
}
}
pub fn tasks_subscription(user_id: &str, project_id: Option<&str>) -> SubscriptionSpec {
let mut scopes = Map::new();
scopes.insert("user_id".to_string(), json!(user_id));
if let Some(project_id) = project_id {
scopes.insert("project_id".to_string(), json!(project_id));
}
let params = Map::new();
SubscriptionSpec {
id: "sub-tasks".to_string(),
table: "tasks".to_string(),
scopes,
params,
bootstrap_phase: 0,
}
}
#[derive(Debug, Clone)]
pub struct NewComment {
pub id: String,
pub task_id: String,
pub project_id: Option<String>,
pub body: String,
pub author_id: String,
pub deleted: i32,
}
impl NewComment {
pub fn new(
id: &str,
task_id: &str,
project_id: Option<&str>,
body: &str,
author_id: &str,
) -> Self {
Self {
id: id.to_string(),
task_id: task_id.to_string(),
project_id: project_id.map(str::to_string),
body: body.to_string(),
author_id: author_id.to_string(),
deleted: 0,
}
}
pub fn with_generated_id(
task_id: &str,
project_id: Option<&str>,
body: &str,
author_id: &str,
) -> Self {
Self {
id: random_syncular_id(),
task_id: task_id.to_string(),
project_id: project_id.map(str::to_string),
body: body.to_string(),
author_id: author_id.to_string(),
deleted: 0,
}
}
pub fn row_json(&self) -> Value {
let mut row = Map::new();
row.insert("id".to_string(), json!(&self.id));
row.insert("task_id".to_string(), json!(&self.task_id));
if let Some(value) = &self.project_id {
row.insert("project_id".to_string(), json!(value));
}
row.insert("body".to_string(), json!(&self.body));
row.insert("author_id".to_string(), json!(&self.author_id));
row.insert("deleted".to_string(), json!(&self.deleted));
Value::Object(row)
}
pub fn sync_operation(&self) -> SyncOperation {
let mut payload = Map::new();
payload.insert("task_id".to_string(), json!(&self.task_id));
if let Some(value) = &self.project_id {
payload.insert("project_id".to_string(), json!(value));
}
payload.insert("body".to_string(), json!(&self.body));
payload.insert("author_id".to_string(), json!(&self.author_id));
payload.insert("deleted".to_string(), json!(&self.deleted));
SyncOperation {
table: "comments".to_string(),
row_id: self.id.clone(),
op: "upsert".to_string(),
payload: Some(Value::Object(payload)),
base_version: Some(0),
}
}
}
impl IntoSyncularMutation for NewComment {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let row_id = self.id.clone();
PendingSyncularMutation {
kind: SyncularMutationKind::Insert,
table: "comments".to_string(),
row_id,
payload: self.sync_operation().payload,
base_version: None,
local_row: Some(self.row_json()),
}
}
}
#[derive(Debug, Clone)]
pub struct CommentPatch {
row_id: String,
base_version: Option<i64>,
task_id: Option<String>,
project_id: Option<Option<String>>,
body: Option<String>,
author_id: Option<String>,
deleted: Option<i32>,
}
impl CommentPatch {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
task_id: None,
project_id: None,
body: None,
author_id: None,
deleted: None,
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn task_id(mut self, task_id: &str) -> Self {
self.task_id = Some(task_id.to_string());
self
}
pub fn project_id(mut self, project_id: Option<&str>) -> Self {
self.project_id = Some(project_id.map(str::to_string));
self
}
pub fn body(mut self, body: &str) -> Self {
self.body = Some(body.to_string());
self
}
pub fn author_id(mut self, author_id: &str) -> Self {
self.author_id = Some(author_id.to_string());
self
}
pub fn deleted(mut self, deleted: i32) -> Self {
self.deleted = Some(deleted);
self
}
pub fn payload_json(&self) -> Value {
let mut payload = Map::new();
if let Some(value) = &self.task_id {
payload.insert("task_id".to_string(), json!(value));
}
if let Some(value) = &self.project_id {
payload.insert("project_id".to_string(), json!(value));
}
if let Some(value) = &self.body {
payload.insert("body".to_string(), json!(value));
}
if let Some(value) = &self.author_id {
payload.insert("author_id".to_string(), json!(value));
}
if let Some(value) = &self.deleted {
payload.insert("deleted".to_string(), json!(value));
}
Value::Object(payload)
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "comments".to_string(),
row_id: self.row_id.clone(),
op: "upsert".to_string(),
payload: Some(self.payload_json()),
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for CommentPatch {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let payload = self.payload_json();
PendingSyncularMutation {
kind: SyncularMutationKind::Update,
table: "comments".to_string(),
row_id: self.row_id,
payload: Some(payload),
base_version: self.base_version,
local_row: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DeleteComment {
row_id: String,
base_version: Option<i64>,
}
impl DeleteComment {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "comments".to_string(),
row_id: self.row_id.clone(),
op: "upsert".to_string(),
payload: Some(json!({ "deleted": 1 })),
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for DeleteComment {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
PendingSyncularMutation {
kind: SyncularMutationKind::Update,
table: "comments".to_string(),
row_id: self.row_id,
payload: Some(json!({ "deleted": 1 })),
base_version: self.base_version,
local_row: None,
}
}
}
pub fn delete_comment(row_id: &str, base_version: Option<i64>) -> SyncOperation {
SyncOperation {
table: "comments".to_string(),
row_id: row_id.to_string(),
op: "upsert".to_string(),
payload: Some(json!({ "deleted": 1 })),
base_version,
}
}
#[derive(Debug, Clone)]
pub struct NewProject {
pub id: String,
pub name: String,
pub owner_id: String,
pub archived: i32,
}
impl NewProject {
pub fn new(id: &str, name: &str, owner_id: &str) -> Self {
Self {
id: id.to_string(),
name: name.to_string(),
owner_id: owner_id.to_string(),
archived: 0,
}
}
pub fn with_generated_id(name: &str, owner_id: &str) -> Self {
Self {
id: random_syncular_id(),
name: name.to_string(),
owner_id: owner_id.to_string(),
archived: 0,
}
}
pub fn row_json(&self) -> Value {
let mut row = Map::new();
row.insert("id".to_string(), json!(&self.id));
row.insert("name".to_string(), json!(&self.name));
row.insert("owner_id".to_string(), json!(&self.owner_id));
row.insert("archived".to_string(), json!(&self.archived));
Value::Object(row)
}
pub fn sync_operation(&self) -> SyncOperation {
let mut payload = Map::new();
payload.insert("name".to_string(), json!(&self.name));
payload.insert("owner_id".to_string(), json!(&self.owner_id));
payload.insert("archived".to_string(), json!(&self.archived));
SyncOperation {
table: "projects".to_string(),
row_id: self.id.clone(),
op: "upsert".to_string(),
payload: Some(Value::Object(payload)),
base_version: Some(0),
}
}
}
impl IntoSyncularMutation for NewProject {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let row_id = self.id.clone();
PendingSyncularMutation {
kind: SyncularMutationKind::Insert,
table: "projects".to_string(),
row_id,
payload: self.sync_operation().payload,
base_version: None,
local_row: Some(self.row_json()),
}
}
}
#[derive(Debug, Clone)]
pub struct ProjectPatch {
row_id: String,
base_version: Option<i64>,
name: Option<String>,
owner_id: Option<String>,
archived: Option<i32>,
}
impl ProjectPatch {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
name: None,
owner_id: None,
archived: None,
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string());
self
}
pub fn owner_id(mut self, owner_id: &str) -> Self {
self.owner_id = Some(owner_id.to_string());
self
}
pub fn archived(mut self, archived: i32) -> Self {
self.archived = Some(archived);
self
}
pub fn payload_json(&self) -> Value {
let mut payload = Map::new();
if let Some(value) = &self.name {
payload.insert("name".to_string(), json!(value));
}
if let Some(value) = &self.owner_id {
payload.insert("owner_id".to_string(), json!(value));
}
if let Some(value) = &self.archived {
payload.insert("archived".to_string(), json!(value));
}
Value::Object(payload)
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "projects".to_string(),
row_id: self.row_id.clone(),
op: "upsert".to_string(),
payload: Some(self.payload_json()),
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for ProjectPatch {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let payload = self.payload_json();
PendingSyncularMutation {
kind: SyncularMutationKind::Update,
table: "projects".to_string(),
row_id: self.row_id,
payload: Some(payload),
base_version: self.base_version,
local_row: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DeleteProject {
row_id: String,
base_version: Option<i64>,
}
impl DeleteProject {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "projects".to_string(),
row_id: self.row_id.clone(),
op: "delete".to_string(),
payload: None,
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for DeleteProject {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
PendingSyncularMutation {
kind: SyncularMutationKind::Delete,
table: "projects".to_string(),
row_id: self.row_id,
payload: None,
base_version: self.base_version,
local_row: None,
}
}
}
pub fn delete_project(row_id: &str, base_version: Option<i64>) -> SyncOperation {
SyncOperation {
table: "projects".to_string(),
row_id: row_id.to_string(),
op: "delete".to_string(),
payload: None,
base_version,
}
}
#[derive(Debug, Clone)]
pub struct NewTask {
pub id: String,
pub title: String,
pub completed: i32,
pub user_id: String,
pub project_id: Option<String>,
pub image: Option<String>,
pub description: Option<String>,
yjs_updates: Map<String, Value>,
}
impl NewTask {
pub fn new(id: &str, title: &str, user_id: &str, project_id: Option<&str>) -> Self {
Self {
id: id.to_string(),
title: title.to_string(),
completed: 0,
user_id: user_id.to_string(),
project_id: project_id.map(str::to_string),
image: None,
description: None,
yjs_updates: Map::new(),
}
}
pub fn with_generated_id(title: &str, user_id: &str, project_id: Option<&str>) -> Self {
Self {
id: random_syncular_id(),
title: title.to_string(),
completed: 0,
user_id: user_id.to_string(),
project_id: project_id.map(str::to_string),
image: None,
description: None,
yjs_updates: Map::new(),
}
}
pub fn title_yjs_update(mut self, update: YjsUpdateEnvelope) -> Self {
self.yjs_updates.insert("title".to_string(), json!(update));
self
}
pub fn title_yjs_updates(mut self, updates: Vec<YjsUpdateEnvelope>) -> Self {
self.yjs_updates.insert("title".to_string(), json!(updates));
self
}
pub fn row_json(&self) -> Value {
let mut row = Map::new();
row.insert("id".to_string(), json!(&self.id));
row.insert("title".to_string(), json!(&self.title));
row.insert("completed".to_string(), json!(&self.completed));
row.insert("user_id".to_string(), json!(&self.user_id));
if let Some(value) = &self.project_id {
row.insert("project_id".to_string(), json!(value));
}
if let Some(value) = &self.image {
row.insert("image".to_string(), json!(value));
}
if let Some(value) = &self.description {
row.insert("description".to_string(), json!(value));
}
Value::Object(row)
}
pub fn sync_operation(&self) -> SyncOperation {
let mut payload = Map::new();
if !self.yjs_updates.contains_key("title") {
payload.insert("title".to_string(), json!(&self.title));
}
payload.insert("completed".to_string(), json!(&self.completed));
payload.insert("user_id".to_string(), json!(&self.user_id));
if let Some(value) = &self.project_id {
payload.insert("project_id".to_string(), json!(value));
}
if let Some(value) = &self.image {
payload.insert("image".to_string(), json!(value));
}
if let Some(value) = &self.description {
payload.insert("description".to_string(), json!(value));
}
if !self.yjs_updates.is_empty() {
payload.insert(
YJS_PAYLOAD_KEY.to_string(),
Value::Object(self.yjs_updates.clone()),
);
}
SyncOperation {
table: "tasks".to_string(),
row_id: self.id.clone(),
op: "upsert".to_string(),
payload: Some(Value::Object(payload)),
base_version: Some(0),
}
}
}
impl IntoSyncularMutation for NewTask {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let row_id = self.id.clone();
PendingSyncularMutation {
kind: SyncularMutationKind::Insert,
table: "tasks".to_string(),
row_id,
payload: self.sync_operation().payload,
base_version: None,
local_row: Some(self.row_json()),
}
}
}
#[derive(Debug, Clone)]
pub struct TaskPatch {
row_id: String,
base_version: Option<i64>,
title: Option<String>,
completed: Option<i32>,
user_id: Option<String>,
project_id: Option<Option<String>>,
image: Option<Option<String>>,
description: Option<Option<String>>,
yjs_updates: Map<String, Value>,
}
impl TaskPatch {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
title: None,
completed: None,
user_id: None,
project_id: None,
image: None,
description: None,
yjs_updates: Map::new(),
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn title(mut self, title: &str) -> Self {
self.title = Some(title.to_string());
self
}
pub fn completed(mut self, completed: i32) -> Self {
self.completed = Some(completed);
self
}
pub fn user_id(mut self, user_id: &str) -> Self {
self.user_id = Some(user_id.to_string());
self
}
pub fn project_id(mut self, project_id: Option<&str>) -> Self {
self.project_id = Some(project_id.map(str::to_string));
self
}
pub fn image(mut self, image: Option<&str>) -> Self {
self.image = Some(image.map(str::to_string));
self
}
pub fn description(mut self, description: Option<&str>) -> Self {
self.description = Some(description.map(str::to_string));
self
}
pub fn title_yjs_update(mut self, update: YjsUpdateEnvelope) -> Self {
self.yjs_updates.insert("title".to_string(), json!(update));
self
}
pub fn title_yjs_updates(mut self, updates: Vec<YjsUpdateEnvelope>) -> Self {
self.yjs_updates.insert("title".to_string(), json!(updates));
self
}
pub fn payload_json(&self) -> Value {
let mut payload = Map::new();
if !self.yjs_updates.contains_key("title") {
if let Some(value) = &self.title {
payload.insert("title".to_string(), json!(value));
}
}
if let Some(value) = &self.completed {
payload.insert("completed".to_string(), json!(value));
}
if let Some(value) = &self.user_id {
payload.insert("user_id".to_string(), json!(value));
}
if let Some(value) = &self.project_id {
payload.insert("project_id".to_string(), json!(value));
}
if let Some(value) = &self.image {
payload.insert("image".to_string(), json!(value));
}
if let Some(value) = &self.description {
payload.insert("description".to_string(), json!(value));
}
if !self.yjs_updates.is_empty() {
payload.insert(
YJS_PAYLOAD_KEY.to_string(),
Value::Object(self.yjs_updates.clone()),
);
}
Value::Object(payload)
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "tasks".to_string(),
row_id: self.row_id.clone(),
op: "upsert".to_string(),
payload: Some(self.payload_json()),
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for TaskPatch {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
let payload = self.payload_json();
PendingSyncularMutation {
kind: SyncularMutationKind::Update,
table: "tasks".to_string(),
row_id: self.row_id,
payload: Some(payload),
base_version: self.base_version,
local_row: None,
}
}
}
#[derive(Debug, Clone)]
pub struct DeleteTask {
row_id: String,
base_version: Option<i64>,
}
impl DeleteTask {
pub fn new(row_id: &str) -> Self {
Self {
row_id: row_id.to_string(),
base_version: None,
}
}
pub fn base_version(mut self, base_version: i64) -> Self {
self.base_version = Some(base_version);
self
}
pub fn sync_operation(&self) -> SyncOperation {
SyncOperation {
table: "tasks".to_string(),
row_id: self.row_id.clone(),
op: "delete".to_string(),
payload: None,
base_version: self.base_version,
}
}
}
impl IntoSyncularMutation for DeleteTask {
fn into_syncular_mutation(self) -> PendingSyncularMutation {
PendingSyncularMutation {
kind: SyncularMutationKind::Delete,
table: "tasks".to_string(),
row_id: self.row_id,
payload: None,
base_version: self.base_version,
local_row: None,
}
}
}
pub fn delete_task(row_id: &str, base_version: Option<i64>) -> SyncOperation {
SyncOperation {
table: "tasks".to_string(),
row_id: row_id.to_string(),
op: "delete".to_string(),
payload: None,
base_version,
}
}
#[derive(Debug, Clone)]
pub struct InsertReceipt {
pub id: String,
pub commit: MutationReceipt,
}
#[derive(Debug, Clone)]
pub struct InsertManyReceipt {
pub ids: Vec<String>,
pub commit: MutationReceipt,
}
pub trait SyncularGeneratedMutationsExt: SyncularMutationExecutor {
fn mutations(&mut self) -> SyncularAppMutations<'_, Self>
where
Self: Sized,
{
SyncularAppMutations { client: self }
}
fn leased_mutations(&mut self) -> SyncularAppLeasedMutations<'_, Self>
where
Self: Sized + SyncularLeasedMutationExecutor,
{
SyncularAppLeasedMutations { client: self }
}
fn command_history(&mut self) -> SyncularAppCommandHistory<'_, Self>
where
Self: Sized + SyncularCommandHistoryExecutor,
{
SyncularAppCommandHistory { client: self }
}
fn commit<R>(
&mut self,
f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
Self: Sized,
{
let mut batch = SyncularMutationBatch::new();
let result = {
let mut tx = SyncularAppMutationTx { batch: &mut batch };
f(&mut tx)?
};
let commit = self.apply_mutation_batch(batch)?;
Ok(MutationCommit { result, commit })
}
fn commit_with_history<R>(
&mut self,
f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
Self: Sized + SyncularCommandHistoryExecutor,
{
syncular_commit_with_history(self, "mutations", f)
}
fn commit_leased<R>(
&mut self,
f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
Self: Sized + SyncularLeasedMutationExecutor,
{
let mut batch = SyncularMutationBatch::new();
let result = {
let mut tx = SyncularAppMutationTx { batch: &mut batch };
f(&mut tx)?
};
let commit = self.apply_leased_mutation_batch(batch)?;
Ok(MutationCommit { result, commit })
}
fn commit_leased_with_history<R>(
&mut self,
f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
Self: Sized + SyncularCommandHistoryExecutor,
{
syncular_commit_with_history(self, "leasedMutations", f)
}
}
impl<C> SyncularGeneratedMutationsExt for C where C: SyncularMutationExecutor {}
pub struct SyncularAppMutations<'a, C: SyncularMutationExecutor + ?Sized> {
client: &'a mut C,
}
pub struct SyncularAppLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
client: &'a mut C,
}
pub struct SyncularAppMutationTx<'a> {
batch: &'a mut SyncularMutationBatch,
}
pub struct SyncularAppCommandHistory<'a, C: SyncularCommandHistoryExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> SyncularAppCommandHistory<'_, C>
where
C: SyncularCommandHistoryExecutor + ?Sized,
{
pub fn can_undo(&mut self) -> Result<bool> {
Ok(self
.client
.command_history_latest(CommandHistoryState::Done)?
.is_some())
}
pub fn can_redo(&mut self) -> Result<bool> {
Ok(self
.client
.command_history_latest(CommandHistoryState::Undone)?
.is_some())
}
pub fn latest_undo(&mut self) -> Result<Option<CommandHistoryRecord>> {
self.client
.command_history_latest(CommandHistoryState::Done)
}
pub fn latest_redo(&mut self) -> Result<Option<CommandHistoryRecord>> {
self.client
.command_history_latest(CommandHistoryState::Undone)
}
pub fn undo_last(&mut self) -> Result<CommandHistoryReceipt> {
syncular_replay_command_history(self.client, CommandHistoryState::Done)
}
pub fn redo_last(&mut self) -> Result<CommandHistoryReceipt> {
syncular_replay_command_history(self.client, CommandHistoryState::Undone)
}
}
fn syncular_commit_with_history<C, R>(
client: &mut C,
mutation_scope: &str,
f: impl FnOnce(&mut SyncularAppMutationTx<'_>) -> Result<R>,
) -> Result<MutationCommit<R>>
where
C: SyncularCommandHistoryExecutor + ?Sized,
{
let mut batch = SyncularMutationBatch::new();
let result = {
let mut tx = SyncularAppMutationTx { batch: &mut batch };
f(&mut tx)?
};
let commit = client.apply_command_history_tracked_batch(mutation_scope, batch)?;
Ok(MutationCommit { result, commit })
}
fn syncular_replay_command_history<C>(
client: &mut C,
state: CommandHistoryState,
) -> Result<CommandHistoryReceipt>
where
C: SyncularCommandHistoryExecutor + ?Sized,
{
let command = client.command_history_latest(state)?.ok_or_else(|| {
syncular_command_history_error(
"sync.command_history_empty",
"there is no Syncular command to replay",
)
})?;
syncular_assert_command_history_safe(&command)?;
let undo = state == CommandHistoryState::Done;
syncular_assert_command_history_current_rows(client, &command, undo)?;
let mut batch = SyncularMutationBatch::new();
if undo {
for entry in command.entries.iter().rev() {
batch.push(syncular_command_history_mutation_for_snapshot(
&entry.table,
&entry.row_id,
&entry.before,
)?);
}
} else {
for entry in &command.entries {
batch.push(syncular_command_history_mutation_for_snapshot(
&entry.table,
&entry.row_id,
&entry.after,
)?);
}
}
let commit = client.apply_command_history_batch(&command.mutation_scope, batch)?;
let next_state = if undo {
CommandHistoryState::Undone
} else {
CommandHistoryState::Done
};
client.command_history_mark(&command.id, next_state, &commit)?;
Ok(CommandHistoryReceipt {
command_id: command.id,
commit,
})
}
fn syncular_assert_command_history_current_rows<C>(
client: &mut C,
command: &CommandHistoryRecord,
undo: bool,
) -> Result<()>
where
C: SyncularCommandHistoryExecutor + ?Sized,
{
for entry in &command.entries {
let current = client.command_history_current_row_json(&entry.table, &entry.row_id)?;
let expected = if undo { &entry.after } else { &entry.before };
if ¤t != expected {
return Err(syncular_command_history_error(
"sync.command_history_conflict",
&format!(
"cannot replay Syncular command {} because {}.{} changed since it was recorded",
command.id, entry.table, entry.row_id
),
));
}
}
Ok(())
}
fn syncular_assert_command_history_safe(command: &CommandHistoryRecord) -> Result<()> {
for entry in &command.entries {
let unsafe_fields = syncular_command_history_unsafe_fields(entry)?;
if !unsafe_fields.is_empty() {
return Err(syncular_command_history_error(
"sync.command_history_unsafe_field",
&format!(
"cannot replay Syncular command {} because {}.{} changed unsafe fields: {}",
command.id,
entry.table,
entry.row_id,
unsafe_fields.join(", ")
),
));
}
}
Ok(())
}
fn syncular_command_history_mutation_for_snapshot(
table: &str,
row_id: &str,
snapshot: &Option<Value>,
) -> Result<PendingSyncularMutation> {
match snapshot {
None => Ok(PendingSyncularMutation {
kind: SyncularMutationKind::Delete,
table: table.to_string(),
row_id: row_id.to_string(),
payload: None,
base_version: None,
local_row: None,
}),
Some(snapshot) => Ok(PendingSyncularMutation {
kind: SyncularMutationKind::Upsert,
table: table.to_string(),
row_id: row_id.to_string(),
payload: Some(syncular_command_history_payload_for_snapshot(
table, snapshot,
)?),
base_version: None,
local_row: None,
}),
}
}
fn syncular_command_history_payload_for_snapshot(table: &str, snapshot: &Value) -> Result<Value> {
let Some(object) = snapshot.as_object() else {
return Err(syncular_command_history_error(
"sync.command_history_conflict",
"command history snapshot must be a JSON object",
));
};
let mut payload: Map<String, Value> = object.clone();
match table {
"comments" => {
payload.remove("id");
payload.remove("server_version");
Ok(())
}
"projects" => {
payload.remove("id");
payload.remove("server_version");
Ok(())
}
"tasks" => {
payload.remove("id");
payload.remove("server_version");
payload.remove("title_yjs_state");
Ok(())
}
_ => Err(syncular_command_history_error(
"sync.command_history_table_unsupported",
&format!("cannot replay undo history for unsupported table {table}"),
)),
}?;
Ok(Value::Object(payload))
}
fn syncular_command_history_unsafe_fields(
entry: &CommandHistoryEntry,
) -> Result<Vec<&'static str>> {
let mut fields = Vec::new();
match entry.table.as_str() {
"comments" => Ok(fields),
"projects" => Ok(fields),
"tasks" => {
syncular_command_history_push_unsafe_field(&mut fields, entry, "image");
syncular_command_history_push_unsafe_field(&mut fields, entry, "description");
syncular_command_history_push_unsafe_field(&mut fields, entry, "title");
syncular_command_history_push_unsafe_field(&mut fields, entry, "title_yjs_state");
Ok(fields)
}
_ => Err(syncular_command_history_error(
"sync.command_history_table_unsupported",
&format!(
"cannot replay undo history for unsupported table {}",
entry.table
),
)),
}
}
fn syncular_command_history_push_unsafe_field(
fields: &mut Vec<&'static str>,
entry: &CommandHistoryEntry,
field: &'static str,
) {
if syncular_command_history_snapshot_field(&entry.before, field)
!= syncular_command_history_snapshot_field(&entry.after, field)
&& !fields.contains(&field)
{
fields.push(field);
}
}
fn syncular_command_history_snapshot_field<'a>(
snapshot: &'a Option<Value>,
field: &str,
) -> Option<&'a Value> {
snapshot.as_ref()?.as_object()?.get(field)
}
fn syncular_command_history_error(code: &str, message: &str) -> SyncularError {
SyncularError::config(format!("{code}: {message}"))
}
impl<'a, C> SyncularAppMutations<'a, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn comments(self) -> CommentMutations<'a, C> {
CommentMutations {
client: self.client,
}
}
}
impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn comments(self) -> CommentLeasedMutations<'a, C> {
CommentLeasedMutations {
client: self.client,
}
}
}
impl<'a> SyncularAppMutationTx<'a> {
pub fn comments(&mut self) -> CommentMutationTx<'_> {
CommentMutationTx { batch: self.batch }
}
}
pub struct CommentMutations<'a, C: SyncularMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> CommentMutations<'_, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewComment) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(
self,
rows: impl IntoIterator<Item = NewComment>,
) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: CommentPatch) -> Result<MutationReceipt> {
self.client.apply_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client.apply_mutation(DeleteComment::new(row_id))
}
}
pub struct CommentLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> CommentLeasedMutations<'_, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewComment) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_leased_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(
self,
rows: impl IntoIterator<Item = NewComment>,
) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_leased_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: CommentPatch) -> Result<MutationReceipt> {
self.client.apply_leased_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client
.apply_leased_mutation(DeleteComment::new(row_id))
}
}
pub struct CommentMutationTx<'a> {
batch: &'a mut SyncularMutationBatch,
}
impl CommentMutationTx<'_> {
pub fn insert(self, row: NewComment) -> Result<String> {
let id = row.id.clone();
self.batch.push(row);
Ok(id)
}
pub fn insert_many(self, rows: impl IntoIterator<Item = NewComment>) -> Result<Vec<String>> {
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
self.batch.push(row);
}
Ok(ids)
}
pub fn update(self, patch: CommentPatch) -> Result<()> {
self.batch.push(patch);
Ok(())
}
pub fn delete(self, row_id: &str) -> Result<()> {
self.batch.push(DeleteComment::new(row_id));
Ok(())
}
}
impl<'a, C> SyncularAppMutations<'a, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn projects(self) -> ProjectMutations<'a, C> {
ProjectMutations {
client: self.client,
}
}
}
impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn projects(self) -> ProjectLeasedMutations<'a, C> {
ProjectLeasedMutations {
client: self.client,
}
}
}
impl<'a> SyncularAppMutationTx<'a> {
pub fn projects(&mut self) -> ProjectMutationTx<'_> {
ProjectMutationTx { batch: self.batch }
}
}
pub struct ProjectMutations<'a, C: SyncularMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> ProjectMutations<'_, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewProject) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(
self,
rows: impl IntoIterator<Item = NewProject>,
) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: ProjectPatch) -> Result<MutationReceipt> {
self.client.apply_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client.apply_mutation(DeleteProject::new(row_id))
}
}
pub struct ProjectLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> ProjectLeasedMutations<'_, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewProject) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_leased_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(
self,
rows: impl IntoIterator<Item = NewProject>,
) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_leased_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: ProjectPatch) -> Result<MutationReceipt> {
self.client.apply_leased_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client
.apply_leased_mutation(DeleteProject::new(row_id))
}
}
pub struct ProjectMutationTx<'a> {
batch: &'a mut SyncularMutationBatch,
}
impl ProjectMutationTx<'_> {
pub fn insert(self, row: NewProject) -> Result<String> {
let id = row.id.clone();
self.batch.push(row);
Ok(id)
}
pub fn insert_many(self, rows: impl IntoIterator<Item = NewProject>) -> Result<Vec<String>> {
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
self.batch.push(row);
}
Ok(ids)
}
pub fn update(self, patch: ProjectPatch) -> Result<()> {
self.batch.push(patch);
Ok(())
}
pub fn delete(self, row_id: &str) -> Result<()> {
self.batch.push(DeleteProject::new(row_id));
Ok(())
}
}
impl<'a, C> SyncularAppMutations<'a, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn tasks(self) -> TaskMutations<'a, C> {
TaskMutations {
client: self.client,
}
}
}
impl<'a, C> SyncularAppLeasedMutations<'a, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn tasks(self) -> TaskLeasedMutations<'a, C> {
TaskLeasedMutations {
client: self.client,
}
}
}
impl<'a> SyncularAppMutationTx<'a> {
pub fn tasks(&mut self) -> TaskMutationTx<'_> {
TaskMutationTx { batch: self.batch }
}
}
pub struct TaskMutations<'a, C: SyncularMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> TaskMutations<'_, C>
where
C: SyncularMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
self.client.apply_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client.apply_mutation(DeleteTask::new(row_id))
}
}
pub struct TaskLeasedMutations<'a, C: SyncularLeasedMutationExecutor + ?Sized> {
client: &'a mut C,
}
impl<C> TaskLeasedMutations<'_, C>
where
C: SyncularLeasedMutationExecutor + ?Sized,
{
pub fn insert(self, row: NewTask) -> Result<InsertReceipt> {
let id = row.id.clone();
let commit = self.client.apply_leased_mutation(row)?;
Ok(InsertReceipt { id, commit })
}
pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<InsertManyReceipt> {
let mut batch = SyncularMutationBatch::new();
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
batch.push(row);
}
let commit = self.client.apply_leased_mutation_batch(batch)?;
Ok(InsertManyReceipt { ids, commit })
}
pub fn update(self, patch: TaskPatch) -> Result<MutationReceipt> {
self.client.apply_leased_mutation(patch)
}
pub fn delete(self, row_id: &str) -> Result<MutationReceipt> {
self.client.apply_leased_mutation(DeleteTask::new(row_id))
}
}
pub struct TaskMutationTx<'a> {
batch: &'a mut SyncularMutationBatch,
}
impl TaskMutationTx<'_> {
pub fn insert(self, row: NewTask) -> Result<String> {
let id = row.id.clone();
self.batch.push(row);
Ok(id)
}
pub fn insert_many(self, rows: impl IntoIterator<Item = NewTask>) -> Result<Vec<String>> {
let mut ids = Vec::new();
for row in rows {
ids.push(row.id.clone());
self.batch.push(row);
}
Ok(ids)
}
pub fn update(self, patch: TaskPatch) -> Result<()> {
self.batch.push(patch);
Ok(())
}
pub fn delete(self, row_id: &str) -> Result<()> {
self.batch.push(DeleteTask::new(row_id));
Ok(())
}
}
pub mod prelude {
pub use super::{
CommentLeasedMutations, CommentMutationTx, CommentMutations, CommentPatch, DeleteComment,
DeleteProject, DeleteTask, InsertManyReceipt, InsertReceipt, NewComment, NewProject,
NewTask, ProjectLeasedMutations, ProjectMutationTx, ProjectMutations, ProjectPatch,
SyncularAppCommandHistory, SyncularAppLeasedMutations, SyncularAppMutationTx,
SyncularAppMutations, SyncularGeneratedMutationsExt, TaskLeasedMutations, TaskMutationTx,
TaskMutations, TaskPatch,
};
}