diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 560fc0354..85b5b7ea8 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -40,24 +40,6 @@ model SharedOperation { @@map("shared_operation") } -model SyncEvent { - id Int @id @default(autoincrement()) - node_id Int - timestamp String - // individual record pub id OR compound many-to-many pub ids - record_id Bytes - // the type of operation, I.E: CREATE, UPDATE, DELETE as an enum - kind Int - // the column name for atomic update operations - column String? - // the new value for create/update operations, msgpack encoded - value String - - node Node @relation(fields: [node_id], references: [id]) - - @@map("sync_event") -} - model Statistics { id Int @id @default(autoincrement()) date_captured DateTime @default(now()) @@ -72,6 +54,7 @@ model Statistics { @@map("statistics") } +/// @local(id: pub_id) model Node { id Int @id @default(autoincrement()) pub_id Bytes @unique @@ -82,10 +65,9 @@ model Node { timezone String? date_created DateTime @default(now()) - sync_events SyncEvent[] - jobs Job[] + jobs Job[] + Location Location[] - Location Location[] OwnedOperation OwnedOperation[] SharedOperation SharedOperation[] @@ -108,6 +90,7 @@ model Volume { @@map("volume") } +/// @owned(id: pub_id) model Location { id Int @id @default(autoincrement()) pub_id Bytes @unique @@ -130,6 +113,7 @@ model Location { @@map("location") } +/// @shared(id: cas_id) model Object { id Int @id @default(autoincrement()) // content addressable storage id - blake3 sampled checksum @@ -174,6 +158,7 @@ model Object { @@map("object") } +/// @shared(id: [location, id]) model FilePath { id Int is_dir Boolean @default(false) @@ -220,6 +205,7 @@ model FileConflict { // keys allow us to know exactly which files can be decrypted with a given key // they can be "mounted" to a client, and then used to decrypt files automatically +/// @shared(id: uuid) model Key { id Int @id @default(autoincrement()) // uuid to identify the key @@ -277,6 +263,7 @@ model MediaData { @@map("media_data") } +/// @shared(id: pub_id) model Tag { id Int @id @default(autoincrement()) pub_id Bytes @unique @@ -376,6 +363,7 @@ model Job { @@map("job") } +/// @shared(id: pub_id) model Album { id Int @id @default(autoincrement()) pub_id Bytes @unique diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index e4de4646a..2c1255265 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -7,6 +7,7 @@ use crate::{ preview::{ThumbnailJob, ThumbnailJobInit}, }, prisma::{file_path, indexer_rules_in_location, location, node, object}, + prisma_sync, }; use rspc::Type; @@ -339,8 +340,9 @@ async fn create_location( .write_op( db, ctx.sync.owned_create( - "Location", - json!({ "id": location_pub_id.as_bytes() }), + prisma_sync::location::SyncId { + pub_id: location_pub_id.as_bytes().to_vec(), + }, [ ("node", json!({ "pub_id": ctx.id.as_bytes() })), ("name", json!(location_name)), diff --git a/core/src/sync/mod.rs b/core/src/sync/mod.rs index 279243df6..e18f529cb 100644 --- a/core/src/sync/mod.rs +++ b/core/src/sync/mod.rs @@ -1,7 +1,6 @@ use futures::future::join_all; use sd_sync::*; -use serde::Deserialize; -use serde_json::{from_slice, from_value, to_vec, Value}; +use serde_json::{from_slice, from_value, json, to_vec, Value}; use std::{ collections::{HashMap, HashSet}, sync::Arc, @@ -10,8 +9,9 @@ use tokio::sync::mpsc::{self, Receiver, Sender}; use uhlc::{HLCBuilder, HLC, NTP64}; use uuid::Uuid; -use crate::prisma::{ - file_path, location, node, object, owned_operation, shared_operation, PrismaClient, +use crate::{ + prisma::{file_path, location, node, object, owned_operation, shared_operation, PrismaClient}, + prisma_sync, }; pub struct SyncManager { @@ -220,19 +220,14 @@ impl SyncManager { match op.typ { CRDTOperationType::Owned(owned_op) => match owned_op.model.as_str() { "FilePath" => { - #[derive(Deserialize)] - struct FilePathId { - location_id: Vec, - id: i32, - } - for item in owned_op.items { - let id: FilePathId = serde_json::from_value(item.id).unwrap(); + let id: prisma_sync::file_path::SyncId = + serde_json::from_value(item.id).unwrap(); let location = self .db .location() - .find_unique(location::pub_id::equals(id.location_id)) + .find_unique(location::pub_id::equals(id.location.pub_id)) .select(location::select!({ id })) .exec() .await? @@ -264,14 +259,17 @@ impl SyncManager { values, skip_duplicates, } => { - let location_ids = values - .iter() - .map(|(id, _)| { - serde_json::from_value::(id.clone()) + let location_ids = + values + .iter() + .map(|(id, _)| { + serde_json::from_value::(id.clone()) .unwrap() - .location_id - }) - .collect::>(); + .location + .pub_id + }) + .collect::>(); + let location_id_mappings = join_all(location_ids.iter().map(|id| async move { self.db @@ -291,12 +289,14 @@ impl SyncManager { values .into_iter() .map(|(id, mut data)| { - let id: FilePathId = + let id: prisma_sync::file_path::SyncId = serde_json::from_value(id).unwrap(); file_path::create_unchecked( id.id, - *location_id_mappings.get(&id.location_id).unwrap(), + *location_id_mappings + .get(&id.location.pub_id) + .unwrap(), serde_json::from_value( data.remove("materialized_path").unwrap(), ) @@ -340,20 +340,15 @@ impl SyncManager { } } "Location" => { - #[derive(Deserialize)] - struct LocationId { - id: Vec, - } - for item in owned_op.items { - let id: LocationId = serde_json::from_value(item.id).unwrap(); + let id: prisma_sync::location::SyncId = from_value(item.id).unwrap(); match item.data { OwnedOperationData::Create(mut data) => { self.db .location() .create( - id.id, + id.pub_id, { let val: std::collections::HashMap = from_value(data.remove("node").unwrap()).unwrap(); @@ -379,15 +374,15 @@ impl SyncManager { }, CRDTOperationType::Shared(shared_op) => match shared_op.model.as_str() { "Object" => { - let cas_id: String = from_value(shared_op.record_id).unwrap(); + let id: prisma_sync::object::SyncId = from_value(shared_op.record_id).unwrap(); match shared_op.data { SharedOperationData::Create(_) => { self.db .object() .upsert( - object::cas_id::equals(cas_id.clone()), - (cas_id, vec![]), + object::cas_id::equals(id.cas_id.clone()), + (id.cas_id, vec![]), vec![], ) .exec() @@ -398,7 +393,7 @@ impl SyncManager { self.db .object() .update( - object::cas_id::equals(cas_id), + object::cas_id::equals(id.cas_id), vec![object::SetParam::deserialize(&field, value).unwrap()], ) .exec() @@ -426,18 +421,21 @@ impl SyncManager { } } - pub fn owned_create( + pub fn owned_create< + const SIZE: usize, + TSyncId: SyncId, + TModel: SyncType, + >( &self, - model: &str, - id: Value, + id: TSyncId, values: [(&'static str, Value); SIZE], ) -> CRDTOperation { self.new_op(CRDTOperationType::Owned(OwnedOperation { - model: model.to_string(), + model: TModel::MODEL.to_string(), items: [(id, values)] .into_iter() .map(|(id, data)| OwnedOperationItem { - id, + id: json!(id), data: OwnedOperationData::Create( data.into_iter().map(|(k, v)| (k.to_string(), v)).collect(), ), diff --git a/crates/sync-generator/src/attribute/mod.rs b/crates/sync-generator/src/attribute/mod.rs index a68a81189..ae780f25a 100644 --- a/crates/sync-generator/src/attribute/mod.rs +++ b/crates/sync-generator/src/attribute/mod.rs @@ -1,3 +1,5 @@ +use prisma_client_rust_sdk::prelude::*; + mod parser; #[derive(Debug)] @@ -14,12 +16,12 @@ impl AttributeFieldValue<'_> { } } - // pub fn as_list(&self) -> Option<&Vec<&str>> { - // match self { - // AttributeFieldValue::List(fields) => Some(fields), - // _ => None, - // } - // } + pub fn as_list(&self) -> Option<&Vec<&str>> { + match self { + AttributeFieldValue::List(fields) => Some(fields), + _ => None, + } + } } #[derive(Debug)] @@ -29,13 +31,23 @@ pub struct Attribute<'a> { } impl<'a> Attribute<'a> { - pub fn parse(input: &'a impl AsRef) -> Result { - parser::parse(input.as_ref()) - .map(|(_, a)| a) - .map_err(|_| ()) + pub fn parse(input: &'a str) -> Result { + parser::parse(input).map(|(_, a)| a).map_err(|_| ()) } pub fn field(&self, name: &str) -> Option<&AttributeFieldValue> { self.fields.iter().find(|(n, _)| *n == name).map(|(_, v)| v) } } + +pub fn model_attributes(model: &dml::Model) -> Vec { + model + .documentation + .as_ref() + .map(|docs| { + docs.lines() + .flat_map(|line| Attribute::parse(line)) + .collect() + }) + .unwrap_or_default() +} diff --git a/crates/sync-generator/src/attribute/parser.rs b/crates/sync-generator/src/attribute/parser.rs index b9cf88f6c..c0e3f635d 100644 --- a/crates/sync-generator/src/attribute/parser.rs +++ b/crates/sync-generator/src/attribute/parser.rs @@ -24,7 +24,7 @@ fn parens(input: &str) -> IResult<&str, &str> { delimited(char('('), is_not(")"), char(')'))(input) } -pub fn single_value>(i: T) -> IResult +fn single_value>(i: T) -> IResult where T: InputTakeAtPosition, ::Item: AsChar, diff --git a/crates/sync-generator/src/lib.rs b/crates/sync-generator/src/lib.rs index b4b584744..10987996a 100644 --- a/crates/sync-generator/src/lib.rs +++ b/crates/sync-generator/src/lib.rs @@ -1,4 +1,8 @@ -pub use prisma_client_rust_sdk::prelude::*; +mod attribute; + +use attribute::*; + +use prisma_client_rust_sdk::prelude::*; #[derive(Debug, serde::Serialize, thiserror::Error)] enum Error {} @@ -6,6 +10,65 @@ enum Error {} #[derive(serde::Deserialize)] struct SDSyncGenerator {} +type FieldVec<'a> = Vec<&'a dml::Field>; + +#[derive(Debug)] +enum ModelSyncType<'a> { + Local { + id: FieldVec<'a>, + }, + Owned { + id: FieldVec<'a>, + }, + Shared { + id: FieldVec<'a>, + }, + Relation { + group: FieldVec<'a>, + item: FieldVec<'a>, + }, +} + +impl<'a> ModelSyncType<'a> { + fn from_attribute(attr: &'a Attribute, model: &'a dml::Model) -> Option { + let id = attr + .field("id") + .map(|field| match field { + AttributeFieldValue::Single(s) => vec![*s], + AttributeFieldValue::List(l) => l.clone(), + }) + .unwrap_or_else(|| { + model + .primary_key + .as_ref() + .unwrap() + .fields + .iter() + .map(|f| f.name.as_str()) + .collect() + }) + .into_iter() + .flat_map(|name| model.find_field(name)) + .collect(); + + Some(match attr.name { + "local" => Self::Local { id }, + "owned" => Self::Owned { id }, + "shared" => Self::Shared { id }, + _ => return None, + }) + } + + fn sync_id(&self) -> Vec<&dml::Field> { + match self { + Self::Owned { id } => id.clone(), + Self::Local { id } => id.clone(), + Self::Shared { id } => id.clone(), + _ => vec![], + } + } +} + impl PrismaGenerator for SDSyncGenerator { const NAME: &'static str = "SD Sync Generator"; const DEFAULT_OUTPUT: &'static str = "prisma-sync.rs"; @@ -13,97 +76,158 @@ impl PrismaGenerator for SDSyncGenerator { type Error = Error; fn generate(self, args: GenerateArgs) -> Result { - let set_param_impls = args.dml.models().map(|model| { - let model_name_snake = snake_ident(&model.name); + let model_modules = args.dml.models().map(|model| { + let model_name_snake = snake_ident(&model.name); - let field_matches = model.fields().filter_map(|field| { - let field_name_snake = snake_ident(field.name()); - let field_name_snake_str = field_name_snake.to_string(); + let attributes = model_attributes(&model); - match field { - dml::Field::ScalarField(_) => { - Some(quote! { - #field_name_snake_str => crate::prisma::#model_name_snake::#field_name_snake::set(::serde_json::from_value(val).unwrap()), - }) - }, - dml::Field::RelationField(relation_field) => { - let relation_model_name_snake = snake_ident(&relation_field.relation_info.referenced_model); + let sync_id = attributes + .iter() + .find_map(|a| ModelSyncType::from_attribute(a, model)) + .map(|sync_type| { + let fields = sync_type.sync_id(); + let fields = fields.iter().flat_map(|field| { + let name_snake = snake_ident(field.name()); - match &relation_field.relation_info.references[..] { - [_] => { - Some(quote! { - #field_name_snake_str => { - let val: std::collections::HashMap = ::serde_json::from_value(val).unwrap(); - let val = val.into_iter().next().unwrap(); - - crate::prisma::#model_name_snake::#field_name_snake::connect( - crate::prisma::#relation_model_name_snake::UniqueWhereParam::deserialize(&val.0, val.1).unwrap() - ) - }, - }) + let typ = match field { + dml::Field::ScalarField(_) => { + field.type_tokens(quote!(self)) }, - _ => None - } - }, - _ => None - } - }); + dml::Field::RelationField(relation)=> { + let relation_model_name_snake = snake_ident(&relation.relation_info.referenced_model); + quote!(super::#relation_model_name_snake::SyncId) + }, + _ => return None + }; - match field_matches.clone().count() { - 0 => quote!(), - _ => quote! { - impl crate::prisma::#model_name_snake::SetParam { - pub fn deserialize(field: &str, val: ::serde_json::Value) -> Option { - Some(match field { - #(#field_matches)* - _ => return None + Some(quote!(pub #name_snake: #typ)) + }); + + let sync_type_marker = match &sync_type { + ModelSyncType::Local { .. } => quote!(LocalSyncType), + ModelSyncType::Owned { .. } => quote!(OwnedSyncType), + ModelSyncType::Shared { .. } => quote!(SharedSyncType), + ModelSyncType::Relation { .. } => quote!(RelationSyncType), + }; + + quote! { + #[derive(serde::Serialize, serde::Deserialize)] + pub struct SyncId { + #(#fields),* + } + + impl sd_sync::SyncId for SyncId { + type ModelTypes = #model_name_snake::Types; + } + + impl sd_sync::SyncType for #model_name_snake::Types { + type SyncId = SyncId; + type Marker = sd_sync::#sync_type_marker; + } + } + }); + + let set_param_impl = { + let field_matches = model.fields().filter_map(|field| { + let field_name_snake = snake_ident(field.name()); + let field_name_snake_str = field_name_snake.to_string(); + + + match field { + dml::Field::ScalarField(_) => { + Some(quote! { + #field_name_snake_str => #model_name_snake::#field_name_snake::set(::serde_json::from_value(val).unwrap()), }) + }, + dml::Field::RelationField(relation_field) => { + let relation_model_name_snake = snake_ident(&relation_field.relation_info.referenced_model); + + match &relation_field.relation_info.references[..] { + [_] => { + Some(quote! { + #field_name_snake_str => { + let val: std::collections::HashMap = ::serde_json::from_value(val).unwrap(); + let val = val.into_iter().next().unwrap(); + + #model_name_snake::#field_name_snake::connect( + #relation_model_name_snake::UniqueWhereParam::deserialize(&val.0, val.1).unwrap() + ) + }, + }) + }, + _ => None + } + }, + _ => None + } + }); + + match field_matches.clone().count() { + 0 => quote!(), + _ => quote! { + impl #model_name_snake::SetParam { + pub fn deserialize(field: &str, val: ::serde_json::Value) -> Option { + Some(match field { + #(#field_matches)* + _ => return None + }) + } } } } + }; + + let unique_param_impl = { + let field_matches = model + .loose_unique_criterias() + .iter() + .flat_map(|criteria| match &criteria.fields[..] { + [field] => { + let unique_field_name_str = &field.name; + let unique_field_name_snake = snake_ident(unique_field_name_str); + + Some(quote!(#unique_field_name_str => + #model_name_snake::#unique_field_name_snake::equals( + ::serde_json::from_value(val).unwrap() + ), + )) + } + _ => None, + }) + .collect::>(); + + match field_matches.len() { + 0 => quote!(), + _ => quote! { + impl #model_name_snake::UniqueWhereParam { + pub fn deserialize(field: &str, val: ::serde_json::Value) -> Option { + Some(match field { + #(#field_matches)* + _ => return None + }) + } + } + }, + } + }; + + quote! { + pub mod #model_name_snake { + use super::prisma::*; + + #sync_id + + #set_param_impl + + #unique_param_impl + } } - }); - - let unique_where_param_impls = args.dml.models().map(|model| { - let model_name_snake = snake_ident(&model.name); - - let field_matches = model - .loose_unique_criterias() - .iter() - .flat_map(|criteria| match &criteria.fields[..] { - [field] => { - let unique_field_name_str = &field.name; - let unique_field_name_snake = snake_ident(unique_field_name_str); - - Some(quote!(#unique_field_name_str => - crate::prisma::#model_name_snake::#unique_field_name_snake::equals( - ::serde_json::from_value(val).unwrap() - ), - )) - } - _ => None, - }) - .collect::>(); - - match field_matches.len() { - 0 => quote!(), - _ => quote! { - impl crate::prisma::#model_name_snake::UniqueWhereParam { - pub fn deserialize(field: &str, val: ::serde_json::Value) -> Option { - Some(match field { - #(#field_matches)* - _ => return None - }) - } - } - }, - } - }); + }); Ok(quote! { - #(#set_param_impls)* + use crate::prisma; - #(#unique_where_param_impls)* + #(#model_modules)* } .to_string()) } diff --git a/crates/sync/src/lib.rs b/crates/sync/src/lib.rs index e9ba7cbac..c0b10c4bf 100644 --- a/crates/sync/src/lib.rs +++ b/crates/sync/src/lib.rs @@ -5,9 +5,33 @@ pub use crdt::*; // pub use db::*; use prisma_client_rust::ModelTypes; +use serde::{de::DeserializeOwned, Serialize}; use serde_value::Value; use std::collections::BTreeMap; +pub trait SyncId: Serialize + DeserializeOwned { + type ModelTypes: SyncType; +} + +pub trait SyncType: ModelTypes { + type SyncId: SyncId; + type Marker: SyncTypeMarker; +} + +pub trait SyncTypeMarker {} + +pub struct LocalSyncType; +impl SyncTypeMarker for LocalSyncType {} + +pub struct OwnedSyncType; +impl SyncTypeMarker for OwnedSyncType {} + +pub struct SharedSyncType; +impl SyncTypeMarker for SharedSyncType {} + +pub struct RelationSyncType; +impl SyncTypeMarker for RelationSyncType {} + pub trait CreateCRDTMutation { fn operation_from_data( d: &BTreeMap, diff --git a/packages/interface/src/components/dialog/AddLocationDialog.tsx b/packages/interface/src/components/dialog/AddLocationDialog.tsx index 3bda286aa..20536fcae 100644 --- a/packages/interface/src/components/dialog/AddLocationDialog.tsx +++ b/packages/interface/src/components/dialog/AddLocationDialog.tsx @@ -1,9 +1,7 @@ import { useLibraryMutation } from '@sd/client'; -import { Input } from '@sd/ui'; import { Dialog } from '@sd/ui'; -import { forms } from '@sd/ui'; -const { useZodForm, z } = forms; +import { Input, useZodForm, z } from '@sd/ui/src/forms'; const schema = z.object({ path: z.string() });