mirror of
https://github.com/spacedriveapp/spacedrive
synced 2024-07-04 08:43:27 +00:00
Stronger linter on sync generator
This commit is contained in:
parent
6cdaed47ec
commit
aa9a117a22
|
@ -28,6 +28,7 @@ pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
||||||
pub p2p_discovery: Option<P2PDiscoveryState>,
|
pub p2p_discovery: Option<P2PDiscoveryState>,
|
||||||
pub p2p_remote_access: Option<bool>,
|
pub p2p_remote_access: Option<bool>,
|
||||||
pub p2p_manual_peers: Option<HashSet<String>>,
|
pub p2p_manual_peers: Option<HashSet<String>>,
|
||||||
|
#[cfg(feature = "ai")]
|
||||||
pub image_labeler_version: Option<String>,
|
pub image_labeler_version: Option<String>,
|
||||||
}
|
}
|
||||||
R.mutation(|node, args: ChangeNodeNameArgs| async move {
|
R.mutation(|node, args: ChangeNodeNameArgs| async move {
|
||||||
|
|
|
@ -10,17 +10,19 @@ pub enum AttributeFieldValue<'a> {
|
||||||
|
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
impl AttributeFieldValue<'_> {
|
impl AttributeFieldValue<'_> {
|
||||||
pub fn as_single(&self) -> Option<&str> {
|
pub const fn as_single(&self) -> Option<&str> {
|
||||||
match self {
|
if let AttributeFieldValue::Single(field) = self {
|
||||||
AttributeFieldValue::Single(field) => Some(field),
|
Some(field)
|
||||||
_ => None,
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_list(&self) -> Option<&Vec<&str>> {
|
pub const fn as_list(&self) -> Option<&Vec<&str>> {
|
||||||
match self {
|
if let AttributeFieldValue::List(fields) = self {
|
||||||
AttributeFieldValue::List(fields) => Some(fields),
|
Some(fields)
|
||||||
_ => None,
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -36,12 +38,14 @@ impl<'a> Attribute<'a> {
|
||||||
parser::parse(input).map(|(_, a)| a).map_err(|_| ())
|
parser::parse(input).map(|(_, a)| a).map_err(|_| ())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn field(&self, name: &str) -> Option<&AttributeFieldValue> {
|
pub fn field(&self, name: &str) -> Option<&AttributeFieldValue<'_>> {
|
||||||
self.fields.iter().find(|(n, _)| *n == name).map(|(_, v)| v)
|
self.fields
|
||||||
|
.iter()
|
||||||
|
.find_map(|(n, v)| (*n == name).then_some(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn model_attributes(model: ModelWalker) -> Vec<Attribute> {
|
pub fn model_attributes(model: ModelWalker<'_>) -> Vec<Attribute<'_>> {
|
||||||
model
|
model
|
||||||
.ast_model()
|
.ast_model()
|
||||||
.documentation()
|
.documentation()
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
use nom::{
|
use nom::{
|
||||||
branch::alt,
|
branch::alt,
|
||||||
bytes::complete::*,
|
bytes::complete::{is_not, tag},
|
||||||
character::complete::*,
|
character::complete::{alpha1, char, multispace0},
|
||||||
combinator::*,
|
combinator::{map, opt},
|
||||||
error::{ErrorKind, ParseError},
|
error::{ErrorKind, ParseError},
|
||||||
multi::*,
|
multi::separated_list1,
|
||||||
sequence::*,
|
sequence::{delimited, separated_pair},
|
||||||
AsChar, IResult, InputTakeAtPosition,
|
AsChar, IResult, InputTakeAtPosition,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,7 +24,7 @@ fn parens(input: &str) -> IResult<&str, &str> {
|
||||||
delimited(char('('), is_not(")"), char(')'))(input)
|
delimited(char('('), is_not(")"), char(')'))(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn single_value<T, E: ParseError<T>>(i: T) -> IResult<T, T, E>
|
fn single_value<T, E: ParseError<T>>(i: &T) -> IResult<T, T, E>
|
||||||
where
|
where
|
||||||
T: InputTakeAtPosition,
|
T: InputTakeAtPosition,
|
||||||
<T as InputTakeAtPosition>::Item: AsChar,
|
<T as InputTakeAtPosition>::Item: AsChar,
|
||||||
|
@ -41,19 +41,19 @@ where
|
||||||
fn list_value(input: &str) -> IResult<&str, Vec<&str>> {
|
fn list_value(input: &str) -> IResult<&str, Vec<&str>> {
|
||||||
delimited(
|
delimited(
|
||||||
char('['),
|
char('['),
|
||||||
separated_list1(char(','), remove_ws(single_value)),
|
separated_list1(char(','), remove_ws(|a| single_value(&a))),
|
||||||
char(']'),
|
char(']'),
|
||||||
)(input)
|
)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attribute_field_value(input: &str) -> IResult<&str, AttributeFieldValue> {
|
fn attribute_field_value(input: &str) -> IResult<&str, AttributeFieldValue<'_>> {
|
||||||
remove_ws(alt((
|
remove_ws(alt((
|
||||||
map(list_value, AttributeFieldValue::List),
|
map(|a| list_value(a), AttributeFieldValue::List),
|
||||||
map(single_value, AttributeFieldValue::Single),
|
map(|a| single_value(&a), AttributeFieldValue::Single),
|
||||||
)))(input)
|
)))(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attribute_field(input: &str) -> IResult<&str, (&str, AttributeFieldValue)> {
|
fn attribute_field(input: &str) -> IResult<&str, (&str, AttributeFieldValue<'_>)> {
|
||||||
remove_ws(separated_pair(
|
remove_ws(separated_pair(
|
||||||
remove_ws(is_not(":")),
|
remove_ws(is_not(":")),
|
||||||
char(':'),
|
char(':'),
|
||||||
|
@ -61,11 +61,11 @@ fn attribute_field(input: &str) -> IResult<&str, (&str, AttributeFieldValue)> {
|
||||||
))(input)
|
))(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn attribute_fields(input: &str) -> IResult<&str, Vec<(&str, AttributeFieldValue)>> {
|
fn attribute_fields(input: &str) -> IResult<&str, Vec<(&str, AttributeFieldValue<'_>)>> {
|
||||||
separated_list1(char(','), attribute_field)(input)
|
separated_list1(char(','), attribute_field)(input)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse(input: &str) -> IResult<&str, Attribute> {
|
pub fn parse(input: &str) -> IResult<&str, Attribute<'_>> {
|
||||||
let (input, _) = remove_ws(tag("@"))(input)?;
|
let (input, _) = remove_ws(tag("@"))(input)?;
|
||||||
let (input, name) = alpha1(input)?;
|
let (input, name) = alpha1(input)?;
|
||||||
let (input, values_str) = opt(remove_ws(parens))(input)?;
|
let (input, values_str) = opt(remove_ws(parens))(input)?;
|
||||||
|
@ -86,7 +86,7 @@ mod test {
|
||||||
fn marker() {
|
fn marker() {
|
||||||
let s = "@local";
|
let s = "@local";
|
||||||
|
|
||||||
let (remaining, attribute) = super::parse(s).unwrap();
|
let (remaining, attribute) = parse(s).unwrap();
|
||||||
|
|
||||||
assert_eq!(remaining, "");
|
assert_eq!(remaining, "");
|
||||||
assert_eq!(attribute.name, "local");
|
assert_eq!(attribute.name, "local");
|
||||||
|
@ -97,7 +97,7 @@ mod test {
|
||||||
fn single() {
|
fn single() {
|
||||||
let s = "@local(foo: bar)";
|
let s = "@local(foo: bar)";
|
||||||
|
|
||||||
let (remaining, attribute) = super::parse(s).unwrap();
|
let (remaining, attribute) = parse(s).unwrap();
|
||||||
|
|
||||||
assert_eq!(remaining, "");
|
assert_eq!(remaining, "");
|
||||||
assert_eq!(attribute.name, "local");
|
assert_eq!(attribute.name, "local");
|
||||||
|
@ -113,7 +113,7 @@ mod test {
|
||||||
fn list() {
|
fn list() {
|
||||||
let s = "@local(foo: [bar, baz])";
|
let s = "@local(foo: [bar, baz])";
|
||||||
|
|
||||||
let (remaining, attribute) = match super::parse(s) {
|
let (remaining, attribute) = match parse(s) {
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
Err(e) => panic!("{}", e),
|
Err(e) => panic!("{}", e),
|
||||||
};
|
};
|
||||||
|
@ -136,7 +136,7 @@ mod test {
|
||||||
fn multiple() {
|
fn multiple() {
|
||||||
let s = "@local(foo: bar, baz: qux)";
|
let s = "@local(foo: bar, baz: qux)";
|
||||||
|
|
||||||
let (remaining, attribute) = super::parse(s).unwrap();
|
let (remaining, attribute) = parse(s).unwrap();
|
||||||
|
|
||||||
assert_eq!(remaining, "");
|
assert_eq!(remaining, "");
|
||||||
assert_eq!(attribute.name, "local");
|
assert_eq!(attribute.name, "local");
|
||||||
|
|
|
@ -1,8 +1,31 @@
|
||||||
mod attribute;
|
#![warn(
|
||||||
mod model;
|
clippy::all,
|
||||||
mod sync_data;
|
clippy::pedantic,
|
||||||
|
clippy::correctness,
|
||||||
use attribute::*;
|
clippy::perf,
|
||||||
|
clippy::style,
|
||||||
|
clippy::suspicious,
|
||||||
|
clippy::complexity,
|
||||||
|
clippy::nursery,
|
||||||
|
clippy::unwrap_used,
|
||||||
|
unused_qualifications,
|
||||||
|
rust_2018_idioms,
|
||||||
|
trivial_casts,
|
||||||
|
trivial_numeric_casts,
|
||||||
|
unused_allocation,
|
||||||
|
clippy::unnecessary_cast,
|
||||||
|
clippy::cast_lossless,
|
||||||
|
clippy::cast_possible_truncation,
|
||||||
|
clippy::cast_possible_wrap,
|
||||||
|
clippy::cast_precision_loss,
|
||||||
|
clippy::cast_sign_loss,
|
||||||
|
clippy::dbg_macro,
|
||||||
|
clippy::deprecated_cfg_attr,
|
||||||
|
clippy::separated_literal_suffix,
|
||||||
|
deprecated
|
||||||
|
)]
|
||||||
|
#![forbid(deprecated_in_future)]
|
||||||
|
#![allow(clippy::missing_errors_doc, clippy::module_name_repetitions)]
|
||||||
|
|
||||||
use prisma_client_rust_sdk::{
|
use prisma_client_rust_sdk::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
|
@ -11,6 +34,12 @@ use prisma_client_rust_sdk::{
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
mod attribute;
|
||||||
|
mod model;
|
||||||
|
mod sync_data;
|
||||||
|
|
||||||
|
use attribute::{model_attributes, Attribute, AttributeFieldValue};
|
||||||
|
|
||||||
#[derive(Debug, serde::Serialize, thiserror::Error)]
|
#[derive(Debug, serde::Serialize, thiserror::Error)]
|
||||||
enum Error {}
|
enum Error {}
|
||||||
|
|
||||||
|
@ -38,7 +67,7 @@ pub enum ModelSyncType<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> ModelSyncType<'a> {
|
impl<'a> ModelSyncType<'a> {
|
||||||
fn from_attribute(attr: Attribute, model: ModelWalker<'a>) -> Option<Self> {
|
fn from_attribute(attr: &Attribute<'_>, model: ModelWalker<'a>) -> Option<Self> {
|
||||||
Some(match attr.name {
|
Some(match attr.name {
|
||||||
"local" | "shared" => {
|
"local" | "shared" => {
|
||||||
let id = attr
|
let id = attr
|
||||||
|
@ -69,14 +98,15 @@ impl<'a> ModelSyncType<'a> {
|
||||||
AttributeFieldValue::List(_) => None,
|
AttributeFieldValue::List(_) => None,
|
||||||
})
|
})
|
||||||
.and_then(|name| {
|
.and_then(|name| {
|
||||||
match model
|
if let RefinedFieldWalker::Relation(r) = model
|
||||||
.fields()
|
.fields()
|
||||||
.find(|f| f.name() == name)
|
.find(|f| f.name() == name)
|
||||||
.unwrap_or_else(|| panic!("'{name}' field not found"))
|
.unwrap_or_else(|| panic!("'{name}' field not found"))
|
||||||
.refine()
|
.refine()
|
||||||
{
|
{
|
||||||
RefinedFieldWalker::Relation(r) => Some(r),
|
Some(r)
|
||||||
_ => None,
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| panic!("'{name}' must be a relation field"))
|
.unwrap_or_else(|| panic!("'{name}' must be a relation field"))
|
||||||
|
@ -96,11 +126,10 @@ impl<'a> ModelSyncType<'a> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_id(&self) -> Vec<FieldWalker> {
|
fn sync_id(&self) -> Vec<FieldWalker<'_>> {
|
||||||
match self {
|
match self {
|
||||||
// Self::Owned { id } => id.clone(),
|
// Self::Owned { id } => id.clone(),
|
||||||
Self::Local { id, .. } => vec![*id],
|
Self::Local { id, .. } | Self::Shared { id, .. } => vec![*id],
|
||||||
Self::Shared { id, .. } => vec![*id],
|
|
||||||
Self::Relation { group, item, .. } => vec![(*group).into(), (*item).into()],
|
Self::Relation { group, item, .. } => vec![(*group).into(), (*item).into()],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +156,7 @@ impl PrismaGenerator for SDSyncGenerator {
|
||||||
|
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
|
|
||||||
fn generate(self, args: GenerateArgs) -> Result<Module, Self::Error> {
|
fn generate(self, args: GenerateArgs<'_>) -> Result<Module, Self::Error> {
|
||||||
let db = &args.schema.db;
|
let db = &args.schema.db;
|
||||||
|
|
||||||
let models_with_sync_types = db
|
let models_with_sync_types = db
|
||||||
|
@ -136,13 +165,13 @@ impl PrismaGenerator for SDSyncGenerator {
|
||||||
.map(|(model, attributes)| {
|
.map(|(model, attributes)| {
|
||||||
let sync_type = attributes
|
let sync_type = attributes
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.find_map(|a| ModelSyncType::from_attribute(a, model));
|
.find_map(|a| ModelSyncType::from_attribute(&a, model));
|
||||||
|
|
||||||
(model, sync_type)
|
(model, sync_type)
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let model_sync_data = sync_data::r#enum(models_with_sync_types.clone());
|
let model_sync_data = sync_data::enumerate(&models_with_sync_types);
|
||||||
|
|
||||||
let mut module = Module::new(
|
let mut module = Module::new(
|
||||||
"root",
|
"root",
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
use prisma_client_rust_sdk::{prelude::*, prisma::prisma_models::walkers::RefinedFieldWalker};
|
use prisma_client_rust_sdk::{prelude::*, prisma::prisma_models::walkers::RefinedFieldWalker};
|
||||||
|
use prisma_models::{ast::ModelId, walkers::Walker};
|
||||||
|
|
||||||
use crate::{ModelSyncType, ModelWithSyncType};
|
use crate::{ModelSyncType, ModelWithSyncType};
|
||||||
|
|
||||||
pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
pub fn module((model, sync_type): ModelWithSyncType<'_>) -> Module {
|
||||||
let model_name_snake = snake_ident(model.name());
|
let model_name_snake = snake_ident(model.name());
|
||||||
|
|
||||||
let sync_id = sync_type.as_ref().map(|sync_type| {
|
let sync_id = sync_type.as_ref().map(|sync_type| {
|
||||||
let fields = sync_type.sync_id();
|
let fields = sync_type.sync_id();
|
||||||
let fields = fields.iter().flat_map(|field| {
|
let fields = fields.iter().map(|field| {
|
||||||
let name_snake = snake_ident(field.name());
|
let name_snake = snake_ident(field.name());
|
||||||
|
|
||||||
let typ = match field.refine() {
|
let typ = match field.refine() {
|
||||||
|
@ -18,58 +19,10 @@ pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(quote!(pub #name_snake: #typ))
|
quote!(pub #name_snake: #typ)
|
||||||
});
|
});
|
||||||
|
|
||||||
let model_stuff = match sync_type {
|
let model_stuff = parse_model(sync_type, &model_name_snake);
|
||||||
ModelSyncType::Relation {
|
|
||||||
item,
|
|
||||||
group,
|
|
||||||
model_id,
|
|
||||||
} => {
|
|
||||||
let item_name_snake = snake_ident(item.name());
|
|
||||||
let item_model_name_snake = snake_ident(item.related_model().name());
|
|
||||||
|
|
||||||
let group_name_snake = snake_ident(group.name());
|
|
||||||
let group_model_name_snake = snake_ident(group.related_model().name());
|
|
||||||
|
|
||||||
Some(quote! {
|
|
||||||
impl sd_sync::RelationSyncId for SyncId {
|
|
||||||
type ItemSyncId = super::#item_model_name_snake::SyncId;
|
|
||||||
type GroupSyncId = super::#group_model_name_snake::SyncId;
|
|
||||||
|
|
||||||
fn split(&self) -> (&Self::ItemSyncId, &Self::GroupSyncId) {
|
|
||||||
(
|
|
||||||
&self.#item_name_snake,
|
|
||||||
&self.#group_name_snake
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const MODEL_ID: u16 = #model_id;
|
|
||||||
|
|
||||||
impl sd_sync::SyncModel for #model_name_snake::Types {
|
|
||||||
const MODEL_ID: u16 = MODEL_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sd_sync::RelationSyncModel for #model_name_snake::Types {
|
|
||||||
type SyncId = SyncId;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
ModelSyncType::Shared { model_id, .. } => Some(quote! {
|
|
||||||
pub const MODEL_ID: u16 = #model_id;
|
|
||||||
|
|
||||||
impl sd_sync::SyncModel for #model_name_snake::Types {
|
|
||||||
const MODEL_ID: u16 = MODEL_ID;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl sd_sync::SharedSyncModel for #model_name_snake::Types {
|
|
||||||
type SyncId = SyncId;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
quote! {
|
||||||
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
|
||||||
|
@ -101,8 +54,9 @@ pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
||||||
let relation_model_name_snake =
|
let relation_model_name_snake =
|
||||||
snake_ident(relation_field.related_model().name());
|
snake_ident(relation_field.related_model().name());
|
||||||
|
|
||||||
match relation_field.referenced_fields() {
|
relation_field.referenced_fields().map_or_else(
|
||||||
Some(i) => {
|
|| None,
|
||||||
|
|i| {
|
||||||
if i.count() == 1 {
|
if i.count() == 1 {
|
||||||
Some(quote! {{
|
Some(quote! {{
|
||||||
let val: std::collections::HashMap<String, rmpv::Value> = ::rmpv::ext::from_value(val).unwrap();
|
let val: std::collections::HashMap<String, rmpv::Value> = ::rmpv::ext::from_value(val).unwrap();
|
||||||
|
@ -115,17 +69,17 @@ pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
_ => None,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.map(|body| quote!(#model_name_snake::#field_name_snake::NAME => #body))
|
.map(|body| quote!(#model_name_snake::#field_name_snake::NAME => #body))
|
||||||
});
|
});
|
||||||
|
|
||||||
match field_matches.clone().count() {
|
if field_matches.clone().count() == 0 {
|
||||||
0 => quote!(),
|
quote!()
|
||||||
_ => quote! {
|
} else {
|
||||||
|
quote! {
|
||||||
impl #model_name_snake::SetParam {
|
impl #model_name_snake::SetParam {
|
||||||
pub fn deserialize(field: &str, val: ::rmpv::Value) -> Option<Self> {
|
pub fn deserialize(field: &str, val: ::rmpv::Value) -> Option<Self> {
|
||||||
Some(match field {
|
Some(match field {
|
||||||
|
@ -134,41 +88,11 @@ pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let unique_param_impl = {
|
let unique_param_impl = process_unique_params(model, &model_name_snake);
|
||||||
let field_matches = model
|
|
||||||
.unique_criterias()
|
|
||||||
.flat_map(|criteria| match &criteria.fields().next() {
|
|
||||||
Some(field) if criteria.fields().len() == 1 => {
|
|
||||||
let field_name_snake = snake_ident(field.name());
|
|
||||||
|
|
||||||
Some(quote!(#model_name_snake::#field_name_snake::NAME =>
|
|
||||||
#model_name_snake::#field_name_snake::equals(
|
|
||||||
::rmpv::ext::from_value(val).unwrap()
|
|
||||||
),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
match field_matches.len() {
|
|
||||||
0 => quote!(),
|
|
||||||
_ => quote! {
|
|
||||||
impl #model_name_snake::UniqueWhereParam {
|
|
||||||
pub fn deserialize(field: &str, val: ::rmpv::Value) -> Option<Self> {
|
|
||||||
Some(match field {
|
|
||||||
#(#field_matches)*
|
|
||||||
_ => return None
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Module::new(
|
Module::new(
|
||||||
model.name(),
|
model.name(),
|
||||||
|
@ -184,3 +108,90 @@ pub fn module((model, sync_type): ModelWithSyncType) -> Module {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn parse_model(sync_type: &ModelSyncType<'_>, model_name_snake: &Ident) -> Option<TokenStream> {
|
||||||
|
match sync_type {
|
||||||
|
ModelSyncType::Relation {
|
||||||
|
item,
|
||||||
|
group,
|
||||||
|
model_id,
|
||||||
|
} => {
|
||||||
|
let item_name_snake = snake_ident(item.name());
|
||||||
|
let item_model_name_snake = snake_ident(item.related_model().name());
|
||||||
|
|
||||||
|
let group_name_snake = snake_ident(group.name());
|
||||||
|
let group_model_name_snake = snake_ident(group.related_model().name());
|
||||||
|
|
||||||
|
Some(quote! {
|
||||||
|
impl sd_sync::RelationSyncId for SyncId {
|
||||||
|
type ItemSyncId = super::#item_model_name_snake::SyncId;
|
||||||
|
type GroupSyncId = super::#group_model_name_snake::SyncId;
|
||||||
|
|
||||||
|
fn split(&self) -> (&Self::ItemSyncId, &Self::GroupSyncId) {
|
||||||
|
(
|
||||||
|
&self.#item_name_snake,
|
||||||
|
&self.#group_name_snake
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const MODEL_ID: u16 = #model_id;
|
||||||
|
|
||||||
|
impl sd_sync::SyncModel for #model_name_snake::Types {
|
||||||
|
const MODEL_ID: u16 = MODEL_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sd_sync::RelationSyncModel for #model_name_snake::Types {
|
||||||
|
type SyncId = SyncId;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
ModelSyncType::Shared { model_id, .. } => Some(quote! {
|
||||||
|
pub const MODEL_ID: u16 = #model_id;
|
||||||
|
|
||||||
|
impl sd_sync::SyncModel for #model_name_snake::Types {
|
||||||
|
const MODEL_ID: u16 = MODEL_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl sd_sync::SharedSyncModel for #model_name_snake::Types {
|
||||||
|
type SyncId = SyncId;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
ModelSyncType::Local { .. } => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn process_unique_params(model: Walker<'_, ModelId>, model_name_snake: &Ident) -> TokenStream {
|
||||||
|
let field_matches = model
|
||||||
|
.unique_criterias()
|
||||||
|
.filter_map(|criteria| match &criteria.fields().next() {
|
||||||
|
Some(field) if criteria.fields().len() == 1 => {
|
||||||
|
let field_name_snake = snake_ident(field.name());
|
||||||
|
|
||||||
|
Some(quote!(#model_name_snake::#field_name_snake::NAME =>
|
||||||
|
#model_name_snake::#field_name_snake::equals(
|
||||||
|
::rmpv::ext::from_value(val).unwrap()
|
||||||
|
),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
if field_matches.is_empty() {
|
||||||
|
quote!()
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
impl #model_name_snake::UniqueWhereParam {
|
||||||
|
pub fn deserialize(field: &str, val: ::rmpv::Value) -> Option<Self> {
|
||||||
|
Some(match field {
|
||||||
|
#(#field_matches)*
|
||||||
|
_ => return None
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,11 @@ use prisma_client_rust_sdk::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
prisma::prisma_models::walkers::{RefinedFieldWalker, RelationFieldWalker},
|
prisma::prisma_models::walkers::{RefinedFieldWalker, RelationFieldWalker},
|
||||||
};
|
};
|
||||||
|
use prisma_models::walkers::{FieldWalker, ScalarFieldWalker};
|
||||||
|
|
||||||
use crate::{ModelSyncType, ModelWithSyncType};
|
use crate::{ModelSyncType, ModelWithSyncType};
|
||||||
|
|
||||||
pub fn r#enum(models: Vec<ModelWithSyncType>) -> TokenStream {
|
pub fn enumerate(models: &[ModelWithSyncType<'_>]) -> TokenStream {
|
||||||
let (variants, matches): (Vec<_>, Vec<_>) = models
|
let (variants, matches): (Vec<_>, Vec<_>) = models
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|(model, sync_type)| {
|
.filter_map(|(model, sync_type)| {
|
||||||
|
@ -38,193 +39,12 @@ pub fn r#enum(models: Vec<ModelWithSyncType>) -> TokenStream {
|
||||||
|
|
||||||
let match_arms = match sync_type.as_ref()? {
|
let match_arms = match sync_type.as_ref()? {
|
||||||
ModelSyncType::Shared { id, model_id } => {
|
ModelSyncType::Shared { id, model_id } => {
|
||||||
let (get_id, equals_value, id_name_snake, create_id) = match id.refine() {
|
handle_crdt_ops_shared(id, *model_id, &model_name_snake)
|
||||||
RefinedFieldWalker::Relation(rel) => {
|
|
||||||
let scalar_field = rel.fields().unwrap().next().unwrap();
|
|
||||||
let id_name_snake = snake_ident(scalar_field.name());
|
|
||||||
let field_name_snake = snake_ident(rel.name());
|
|
||||||
let opposite_model_name_snake =
|
|
||||||
snake_ident(rel.opposite_relation_field().unwrap().model().name());
|
|
||||||
|
|
||||||
let relation_equals_condition = quote!(prisma::#opposite_model_name_snake::pub_id::equals(
|
|
||||||
id.#field_name_snake.pub_id.clone()
|
|
||||||
));
|
|
||||||
|
|
||||||
let rel_fetch = quote! {
|
|
||||||
let rel = db.#opposite_model_name_snake()
|
|
||||||
.find_unique(#relation_equals_condition)
|
|
||||||
.exec()
|
|
||||||
.await?
|
|
||||||
.unwrap();
|
|
||||||
};
|
|
||||||
|
|
||||||
(
|
|
||||||
Some(rel_fetch),
|
|
||||||
quote!(rel.id),
|
|
||||||
id_name_snake,
|
|
||||||
relation_equals_condition,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RefinedFieldWalker::Scalar(s) => {
|
|
||||||
let field_name_snake = snake_ident(s.name());
|
|
||||||
let thing = quote!(id.#field_name_snake.clone());
|
|
||||||
|
|
||||||
(None, thing.clone(), field_name_snake, thing)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
#get_id
|
|
||||||
|
|
||||||
match data {
|
|
||||||
sd_sync::CRDTOperationData::Create(data) => {
|
|
||||||
let data: Vec<_> = data.into_iter().map(|(field, value)| {
|
|
||||||
prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()
|
|
||||||
}).collect();
|
|
||||||
|
|
||||||
db.#model_name_snake()
|
|
||||||
.upsert(
|
|
||||||
prisma::#model_name_snake::#id_name_snake::equals(#equals_value),
|
|
||||||
prisma::#model_name_snake::create(#create_id, data.clone()),
|
|
||||||
data
|
|
||||||
)
|
|
||||||
.exec()
|
|
||||||
.await?;
|
|
||||||
},
|
|
||||||
sd_sync::CRDTOperationData::Update { field, value } => {
|
|
||||||
let data = vec![
|
|
||||||
prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()
|
|
||||||
];
|
|
||||||
|
|
||||||
db.#model_name_snake()
|
|
||||||
.upsert(
|
|
||||||
prisma::#model_name_snake::#id_name_snake::equals(#equals_value),
|
|
||||||
prisma::#model_name_snake::create(#create_id, data.clone()),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
.exec()
|
|
||||||
.await?;
|
|
||||||
},
|
|
||||||
sd_sync::CRDTOperationData::Delete => {
|
|
||||||
db.#model_name_snake()
|
|
||||||
.delete(prisma::#model_name_snake::#id_name_snake::equals(#equals_value))
|
|
||||||
.exec()
|
|
||||||
.await?;
|
|
||||||
|
|
||||||
db.crdt_operation()
|
|
||||||
.delete_many(vec![
|
|
||||||
prisma::crdt_operation::model::equals(#model_id as i32),
|
|
||||||
prisma::crdt_operation::record_id::equals(rmp_serde::to_vec(&id).unwrap()),
|
|
||||||
prisma::crdt_operation::kind::equals(sd_sync::OperationKind::Create.to_string())
|
|
||||||
])
|
|
||||||
.exec()
|
|
||||||
.await?;
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ModelSyncType::Relation { item, group, .. } => {
|
ModelSyncType::Relation { item, group, .. } => {
|
||||||
let compound_id = format_ident!(
|
handle_crdt_ops_relation(models, item, group, &model_name_snake)
|
||||||
"{}",
|
|
||||||
group
|
|
||||||
.fields()
|
|
||||||
.unwrap()
|
|
||||||
.chain(item.fields().unwrap())
|
|
||||||
.map(|f| f.name())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join("_")
|
|
||||||
);
|
|
||||||
|
|
||||||
let db_batch_items = {
|
|
||||||
let batch_item = |item: &RelationFieldWalker| {
|
|
||||||
let item_model_sync_id_field_name_snake = models
|
|
||||||
.iter()
|
|
||||||
.find(|m| m.0.name() == item.related_model().name())
|
|
||||||
.and_then(|(_m, sync)| sync.as_ref())
|
|
||||||
.map(|sync| snake_ident(sync.sync_id()[0].name()))
|
|
||||||
.unwrap();
|
|
||||||
let item_model_name_snake = snake_ident(item.related_model().name());
|
|
||||||
let item_field_name_snake = snake_ident(item.name());
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
db.#item_model_name_snake()
|
|
||||||
.find_unique(
|
|
||||||
prisma::#item_model_name_snake::#item_model_sync_id_field_name_snake::equals(
|
|
||||||
id.#item_field_name_snake.#item_model_sync_id_field_name_snake.clone()
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.select(prisma::#item_model_name_snake::select!({ id }))
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
[batch_item(group), batch_item(item)]
|
|
||||||
};
|
|
||||||
|
|
||||||
let create_items = {
|
|
||||||
let create_item = |item: &RelationFieldWalker, var: TokenStream| {
|
|
||||||
let item_model_name_snake = snake_ident(item.related_model().name());
|
|
||||||
|
|
||||||
quote!(
|
|
||||||
prisma::#item_model_name_snake::id::equals(#var.id)
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
[
|
|
||||||
create_item(item, quote!(item)),
|
|
||||||
create_item(group, quote!(group)),
|
|
||||||
]
|
|
||||||
};
|
|
||||||
|
|
||||||
quote! {
|
|
||||||
let (Some(group), Some(item)) =
|
|
||||||
(#(#db_batch_items.exec().await?),*) else {
|
|
||||||
panic!("item and group not found!");
|
|
||||||
};
|
|
||||||
|
|
||||||
let id = prisma::#model_name_snake::#compound_id(group.id, item.id);
|
|
||||||
|
|
||||||
match data {
|
|
||||||
sd_sync::CRDTOperationData::Create(_) => {
|
|
||||||
db.#model_name_snake()
|
|
||||||
.upsert(
|
|
||||||
id,
|
|
||||||
prisma::#model_name_snake::create(
|
|
||||||
#(#create_items),*,
|
|
||||||
vec![]
|
|
||||||
),
|
|
||||||
vec![],
|
|
||||||
)
|
|
||||||
.exec()
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
},
|
|
||||||
sd_sync::CRDTOperationData::Update { field, value } => {
|
|
||||||
let data = vec![prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()];
|
|
||||||
|
|
||||||
db.#model_name_snake()
|
|
||||||
.upsert(
|
|
||||||
id,
|
|
||||||
prisma::#model_name_snake::create(
|
|
||||||
#(#create_items),*,
|
|
||||||
data.clone(),
|
|
||||||
),
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
.exec()
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
},
|
|
||||||
sd_sync::CRDTOperationData::Delete => {
|
|
||||||
db.#model_name_snake()
|
|
||||||
.delete(id)
|
|
||||||
.exec()
|
|
||||||
.await
|
|
||||||
.ok();
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_ => return None,
|
ModelSyncType::Local { .. } => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
Some(quote! {
|
Some(quote! {
|
||||||
|
@ -257,3 +77,210 @@ pub fn r#enum(models: Vec<ModelWithSyncType>) -> TokenStream {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_crdt_ops_relation(
|
||||||
|
models: &[ModelWithSyncType<'_>],
|
||||||
|
item: &RelationFieldWalker<'_>,
|
||||||
|
group: &RelationFieldWalker<'_>,
|
||||||
|
model_name_snake: &Ident,
|
||||||
|
) -> TokenStream {
|
||||||
|
let compound_id = format_ident!(
|
||||||
|
"{}",
|
||||||
|
group
|
||||||
|
.fields()
|
||||||
|
.expect("missing group fields")
|
||||||
|
.chain(item.fields().expect("missing item fields"))
|
||||||
|
.map(ScalarFieldWalker::name)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("_")
|
||||||
|
);
|
||||||
|
|
||||||
|
let db_batch_items = {
|
||||||
|
let batch_item = |item: &RelationFieldWalker<'_>| {
|
||||||
|
let item_model_sync_id_field_name_snake = models
|
||||||
|
.iter()
|
||||||
|
.find(|m| m.0.name() == item.related_model().name())
|
||||||
|
.and_then(|(_m, sync)| sync.as_ref())
|
||||||
|
.map(|sync| snake_ident(sync.sync_id()[0].name()))
|
||||||
|
.expect("missing sync id field name for relation");
|
||||||
|
let item_model_name_snake = snake_ident(item.related_model().name());
|
||||||
|
let item_field_name_snake = snake_ident(item.name());
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
db.#item_model_name_snake()
|
||||||
|
.find_unique(
|
||||||
|
prisma::#item_model_name_snake::#item_model_sync_id_field_name_snake::equals(
|
||||||
|
id.#item_field_name_snake.#item_model_sync_id_field_name_snake.clone()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.select(prisma::#item_model_name_snake::select!({ id }))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
[batch_item(group), batch_item(item)]
|
||||||
|
};
|
||||||
|
|
||||||
|
let create_items = {
|
||||||
|
let create_item = |item: &RelationFieldWalker<'_>, var: TokenStream| {
|
||||||
|
let item_model_name_snake = snake_ident(item.related_model().name());
|
||||||
|
|
||||||
|
quote!(
|
||||||
|
prisma::#item_model_name_snake::id::equals(#var.id)
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
[
|
||||||
|
create_item(item, quote!(item)),
|
||||||
|
create_item(group, quote!(group)),
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
let (Some(group), Some(item)) =
|
||||||
|
(#(#db_batch_items.exec().await?),*) else {
|
||||||
|
panic!("item and group not found!");
|
||||||
|
};
|
||||||
|
|
||||||
|
let id = prisma::#model_name_snake::#compound_id(group.id, item.id);
|
||||||
|
|
||||||
|
match data {
|
||||||
|
sd_sync::CRDTOperationData::Create(_) => {
|
||||||
|
db.#model_name_snake()
|
||||||
|
.upsert(
|
||||||
|
id,
|
||||||
|
prisma::#model_name_snake::create(
|
||||||
|
#(#create_items),*,
|
||||||
|
vec![]
|
||||||
|
),
|
||||||
|
vec![],
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
},
|
||||||
|
sd_sync::CRDTOperationData::Update { field, value } => {
|
||||||
|
let data = vec![prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()];
|
||||||
|
|
||||||
|
db.#model_name_snake()
|
||||||
|
.upsert(
|
||||||
|
id,
|
||||||
|
prisma::#model_name_snake::create(
|
||||||
|
#(#create_items),*,
|
||||||
|
data.clone(),
|
||||||
|
),
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
},
|
||||||
|
sd_sync::CRDTOperationData::Delete => {
|
||||||
|
db.#model_name_snake()
|
||||||
|
.delete(id)
|
||||||
|
.exec()
|
||||||
|
.await
|
||||||
|
.ok();
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn handle_crdt_ops_shared(
|
||||||
|
id: &FieldWalker<'_>,
|
||||||
|
model_id: u16,
|
||||||
|
model_name_snake: &Ident,
|
||||||
|
) -> TokenStream {
|
||||||
|
let (get_id, equals_value, id_name_snake, create_id) = match id.refine() {
|
||||||
|
RefinedFieldWalker::Relation(rel) => {
|
||||||
|
let scalar_field = rel
|
||||||
|
.fields()
|
||||||
|
.expect("missing fields")
|
||||||
|
.next()
|
||||||
|
.expect("empty fields");
|
||||||
|
let id_name_snake = snake_ident(scalar_field.name());
|
||||||
|
let field_name_snake = snake_ident(rel.name());
|
||||||
|
let opposite_model_name_snake = snake_ident(
|
||||||
|
rel.opposite_relation_field()
|
||||||
|
.expect("missing opposite relation field")
|
||||||
|
.model()
|
||||||
|
.name(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let relation_equals_condition = quote!(prisma::#opposite_model_name_snake::pub_id::equals(
|
||||||
|
id.#field_name_snake.pub_id.clone()
|
||||||
|
));
|
||||||
|
|
||||||
|
let rel_fetch = quote! {
|
||||||
|
let rel = db.#opposite_model_name_snake()
|
||||||
|
.find_unique(#relation_equals_condition)
|
||||||
|
.exec()
|
||||||
|
.await?
|
||||||
|
.unwrap();
|
||||||
|
};
|
||||||
|
|
||||||
|
(
|
||||||
|
Some(rel_fetch),
|
||||||
|
quote!(rel.id),
|
||||||
|
id_name_snake,
|
||||||
|
relation_equals_condition,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RefinedFieldWalker::Scalar(s) => {
|
||||||
|
let field_name_snake = snake_ident(s.name());
|
||||||
|
let thing = quote!(id.#field_name_snake.clone());
|
||||||
|
|
||||||
|
(None, thing.clone(), field_name_snake, thing)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
quote! {
|
||||||
|
#get_id
|
||||||
|
|
||||||
|
match data {
|
||||||
|
sd_sync::CRDTOperationData::Create(data) => {
|
||||||
|
let data: Vec<_> = data.into_iter().map(|(field, value)| {
|
||||||
|
prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
db.#model_name_snake()
|
||||||
|
.upsert(
|
||||||
|
prisma::#model_name_snake::#id_name_snake::equals(#equals_value),
|
||||||
|
prisma::#model_name_snake::create(#create_id, data.clone()),
|
||||||
|
data
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
},
|
||||||
|
sd_sync::CRDTOperationData::Update { field, value } => {
|
||||||
|
let data = vec![
|
||||||
|
prisma::#model_name_snake::SetParam::deserialize(&field, value).unwrap()
|
||||||
|
];
|
||||||
|
|
||||||
|
db.#model_name_snake()
|
||||||
|
.upsert(
|
||||||
|
prisma::#model_name_snake::#id_name_snake::equals(#equals_value),
|
||||||
|
prisma::#model_name_snake::create(#create_id, data.clone()),
|
||||||
|
data,
|
||||||
|
)
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
},
|
||||||
|
sd_sync::CRDTOperationData::Delete => {
|
||||||
|
db.#model_name_snake()
|
||||||
|
.delete(prisma::#model_name_snake::#id_name_snake::equals(#equals_value))
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
db.crdt_operation()
|
||||||
|
.delete_many(vec![
|
||||||
|
prisma::crdt_operation::model::equals(#model_id as i32),
|
||||||
|
prisma::crdt_operation::record_id::equals(rmp_serde::to_vec(&id).unwrap()),
|
||||||
|
prisma::crdt_operation::kind::equals(sd_sync::OperationKind::Create.to_string())
|
||||||
|
])
|
||||||
|
.exec()
|
||||||
|
.await?;
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -169,7 +169,7 @@ export type CameraData = { device_make: string | null; device_model: string | nu
|
||||||
|
|
||||||
export type CasId = string
|
export type CasId = string
|
||||||
|
|
||||||
export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_disabled: boolean | null; p2p_ipv6_disabled: boolean | null; p2p_relay_disabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; p2p_manual_peers: string[] | null; image_labeler_version: string | null }
|
export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p2p_disabled: boolean | null; p2p_ipv6_disabled: boolean | null; p2p_relay_disabled: boolean | null; p2p_discovery: P2PDiscoveryState | null; p2p_remote_access: boolean | null; p2p_manual_peers: string[] | null }
|
||||||
|
|
||||||
export type Chapter = { id: number; start: [number, number]; end: [number, number]; time_base_den: number; time_base_num: number; metadata: Metadata }
|
export type Chapter = { id: number; start: [number, number]; end: [number, number]; time_base_den: number; time_base_num: number; metadata: Metadata }
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue