use std::collections::HashMap; use std::hash::Hasher; use super::ids::*; use super::DatabaseError; use crate::database::redis::RedisPool; use chrono::DateTime; use chrono::Utc; use dashmap::DashMap; use futures::TryStreamExt; use itertools::Itertools; use serde::{Deserialize, Serialize}; const GAMES_LIST_NAMESPACE: &str = "games"; const LOADER_ID: &str = "loader_id"; const LOADERS_LIST_NAMESPACE: &str = "loaders"; const LOADER_FIELDS_NAMESPACE: &str = "loader_fields"; const LOADER_FIELDS_NAMESPACE_ALL: &str = "loader_fields_all"; const LOADER_FIELD_ENUMS_ID_NAMESPACE: &str = "loader_field_enums"; pub const LOADER_FIELD_ENUM_VALUES_NAMESPACE: &str = "loader_field_enum_values"; #[derive(Clone, Serialize, Deserialize, Debug)] pub struct Game { pub id: GameId, pub slug: String, pub name: String, pub icon_url: Option, pub banner_url: Option, } impl Game { pub async fn get_slug<'a, E>( slug: &str, exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { Ok(Self::list(exec, redis) .await? .into_iter() .find(|x| x.slug == slug)) } pub async fn list<'a, E>( exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; let cached_games: Option> = redis .get_deserialized_from_json(GAMES_LIST_NAMESPACE, "games") .await?; if let Some(cached_games) = cached_games { return Ok(cached_games); } let result = sqlx::query!( " SELECT id, slug, name, icon_url, banner_url FROM games ", ) .fetch(exec) .map_ok(|x| Game { id: GameId(x.id), slug: x.slug, name: x.name, icon_url: x.icon_url, banner_url: x.banner_url, }) .try_collect::>() .await?; redis .set_serialized_to_json( GAMES_LIST_NAMESPACE, "games", &result, None, ) .await?; Ok(result) } } #[derive(Serialize, Deserialize, Clone)] pub struct Loader { pub id: LoaderId, pub loader: String, pub icon: String, pub supported_project_types: Vec, pub supported_games: Vec, // slugs pub metadata: serde_json::Value, } impl Loader { pub async fn get_id<'a, E>( name: &str, exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; let cached_id: Option = redis.get_deserialized_from_json(LOADER_ID, name).await?; if let Some(cached_id) = cached_id { return Ok(Some(LoaderId(cached_id))); } let result = sqlx::query!( " SELECT id FROM loaders WHERE loader = $1 ", name ) .fetch_optional(exec) .await? .map(|r| LoaderId(r.id)); if let Some(result) = result { redis .set_serialized_to_json(LOADER_ID, name, &result.0, None) .await?; } Ok(result) } pub async fn list<'a, E>( exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; let cached_loaders: Option> = redis .get_deserialized_from_json(LOADERS_LIST_NAMESPACE, "all") .await?; if let Some(cached_loaders) = cached_loaders { return Ok(cached_loaders); } let result = sqlx::query!( " SELECT l.id id, l.loader loader, l.icon icon, l.metadata metadata, ARRAY_AGG(DISTINCT pt.name) filter (where pt.name is not null) project_types, ARRAY_AGG(DISTINCT g.slug) filter (where g.slug is not null) games FROM loaders l LEFT OUTER JOIN loaders_project_types lpt ON joining_loader_id = l.id LEFT OUTER JOIN project_types pt ON lpt.joining_project_type_id = pt.id LEFT OUTER JOIN loaders_project_types_games lptg ON lptg.loader_id = lpt.joining_loader_id AND lptg.project_type_id = lpt.joining_project_type_id LEFT OUTER JOIN games g ON lptg.game_id = g.id GROUP BY l.id; ", ) .fetch(exec) .map_ok(|x| Loader { id: LoaderId(x.id), loader: x.loader, icon: x.icon, supported_project_types: x .project_types .unwrap_or_default() .iter() .map(|x| x.to_string()) .collect(), supported_games: x .games .unwrap_or_default(), metadata: x.metadata }) .try_collect::>() .await?; redis .set_serialized_to_json( LOADERS_LIST_NAMESPACE, "all", &result, None, ) .await?; Ok(result) } } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct LoaderField { pub id: LoaderFieldId, pub field: String, pub field_type: LoaderFieldType, pub optional: bool, pub min_val: Option, pub max_val: Option, } #[derive(Clone, Serialize, Deserialize, Debug)] pub enum LoaderFieldType { Integer, Text, Enum(LoaderFieldEnumId), Boolean, ArrayInteger, ArrayText, ArrayEnum(LoaderFieldEnumId), ArrayBoolean, } impl LoaderFieldType { pub fn build( field_type_name: &str, loader_field_enum: Option, ) -> Option { Some(match (field_type_name, loader_field_enum) { ("integer", _) => LoaderFieldType::Integer, ("text", _) => LoaderFieldType::Text, ("boolean", _) => LoaderFieldType::Boolean, ("array_integer", _) => LoaderFieldType::ArrayInteger, ("array_text", _) => LoaderFieldType::ArrayText, ("array_boolean", _) => LoaderFieldType::ArrayBoolean, ("enum", Some(id)) => LoaderFieldType::Enum(LoaderFieldEnumId(id)), ("array_enum", Some(id)) => { LoaderFieldType::ArrayEnum(LoaderFieldEnumId(id)) } _ => return None, }) } pub fn to_str(&self) -> &'static str { match self { LoaderFieldType::Integer => "integer", LoaderFieldType::Text => "text", LoaderFieldType::Boolean => "boolean", LoaderFieldType::ArrayInteger => "array_integer", LoaderFieldType::ArrayText => "array_text", LoaderFieldType::ArrayBoolean => "array_boolean", LoaderFieldType::Enum(_) => "enum", LoaderFieldType::ArrayEnum(_) => "array_enum", } } pub fn is_array(&self) -> bool { match self { LoaderFieldType::ArrayInteger => true, LoaderFieldType::ArrayText => true, LoaderFieldType::ArrayBoolean => true, LoaderFieldType::ArrayEnum(_) => true, LoaderFieldType::Integer => false, LoaderFieldType::Text => false, LoaderFieldType::Boolean => false, LoaderFieldType::Enum(_) => false, } } } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct LoaderFieldEnum { pub id: LoaderFieldEnumId, pub enum_name: String, pub ordering: Option, pub hidable: bool, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] pub struct LoaderFieldEnumValue { pub id: LoaderFieldEnumValueId, pub enum_id: LoaderFieldEnumId, pub value: String, pub ordering: Option, pub created: DateTime, #[serde(flatten)] pub metadata: serde_json::Value, } impl std::hash::Hash for LoaderFieldEnumValue { fn hash(&self, state: &mut H) { self.id.hash(state); self.enum_id.hash(state); self.value.hash(state); self.ordering.hash(state); self.created.hash(state); } } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] pub struct VersionField { pub version_id: VersionId, pub field_id: LoaderFieldId, pub field_name: String, pub value: VersionFieldValue, } #[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq, Hash)] pub enum VersionFieldValue { Integer(i32), Text(String), Enum(LoaderFieldEnumId, LoaderFieldEnumValue), Boolean(bool), ArrayInteger(Vec), ArrayText(Vec), ArrayEnum(LoaderFieldEnumId, Vec), ArrayBoolean(Vec), } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct QueryVersionField { pub version_id: VersionId, pub field_id: LoaderFieldId, pub int_value: Option, pub enum_value: Option, pub string_value: Option, } impl QueryVersionField { pub fn with_int_value(mut self, int_value: i32) -> Self { self.int_value = Some(int_value); self } pub fn with_enum_value( mut self, enum_value: LoaderFieldEnumValueId, ) -> Self { self.enum_value = Some(enum_value); self } pub fn with_string_value(mut self, string_value: String) -> Self { self.string_value = Some(string_value); self } } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct QueryLoaderField { pub id: LoaderFieldId, pub field: String, pub field_type: String, pub enum_type: Option, pub min_val: Option, pub max_val: Option, pub optional: bool, } #[derive(Clone, Serialize, Deserialize, Debug)] pub struct QueryLoaderFieldEnumValue { pub id: LoaderFieldEnumValueId, pub enum_id: LoaderFieldEnumId, pub value: String, pub ordering: Option, pub created: DateTime, pub metadata: Option, } impl LoaderField { pub async fn get_field<'a, E>( field: &str, loader_ids: &[LoaderId], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let fields = Self::get_fields(loader_ids, exec, redis).await?; Ok(fields.into_iter().find(|f| f.field == field)) } // Gets all fields for a given loader(s) // Returns all as this there are probably relatively few fields per loader pub async fn get_fields<'a, E>( loader_ids: &[LoaderId], exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let found_loader_fields = Self::get_fields_per_loader(loader_ids, exec, redis).await?; let result = found_loader_fields .into_values() .flatten() .unique_by(|x| x.id) .collect(); Ok(result) } pub async fn get_fields_per_loader<'a, E>( loader_ids: &[LoaderId], exec: E, redis: &RedisPool, ) -> Result>, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let val = redis.get_cached_keys_raw( LOADER_FIELDS_NAMESPACE, &loader_ids.iter().map(|x| x.0).collect::>(), |loader_ids| async move { let result = sqlx::query!( " SELECT DISTINCT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type, lfl.loader_id FROM loader_fields lf LEFT JOIN loader_fields_loaders lfl ON lfl.loader_field_id = lf.id WHERE lfl.loader_id = ANY($1) ", &loader_ids, ) .fetch(exec) .try_fold(DashMap::new(), |acc: DashMap>, r| { if let Some(field_type) = LoaderFieldType::build(&r.field_type, r.enum_type) { let loader_field = LoaderField { id: LoaderFieldId(r.id), field_type, field: r.field, optional: r.optional, min_val: r.min_val, max_val: r.max_val, }; acc.entry(r.loader_id) .or_default() .push(loader_field); } async move { Ok(acc) } }) .await?; Ok(result) }, ).await?; Ok(val.into_iter().map(|x| (LoaderId(x.0), x.1)).collect()) } // Gets all fields for a given loader(s) // This is for tags, which need all fields for all loaders // We want to return them even in testing situations where we dont have loaders or loader_fields_loaders set up pub async fn get_fields_all<'a, E>( exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; let cached_fields: Option> = redis .get(LOADER_FIELDS_NAMESPACE_ALL, "") .await? .and_then(|x| serde_json::from_str::>(&x).ok()); if let Some(cached_fields) = cached_fields { return Ok(cached_fields); } let result = sqlx::query!( " SELECT DISTINCT lf.id, lf.field, lf.field_type, lf.optional, lf.min_val, lf.max_val, lf.enum_type FROM loader_fields lf ", ) .fetch(exec) .map_ok(|r| { Some(LoaderField { id: LoaderFieldId(r.id), field_type: LoaderFieldType::build(&r.field_type, r.enum_type)?, field: r.field, optional: r.optional, min_val: r.min_val, max_val: r.max_val, }) }) .try_collect::>>() .await? .into_iter() .flatten() .collect(); redis .set_serialized_to_json( LOADER_FIELDS_NAMESPACE_ALL, "", &result, None, ) .await?; Ok(result) } } impl LoaderFieldEnum { pub async fn get<'a, E>( enum_name: &str, // Note: NOT loader field name exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let mut redis = redis.connect().await?; let cached_enum = redis .get_deserialized_from_json( LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name, ) .await?; if let Some(cached_enum) = cached_enum { return Ok(cached_enum); } let result = sqlx::query!( " SELECT lfe.id, lfe.enum_name, lfe.ordering, lfe.hidable FROM loader_field_enums lfe WHERE lfe.enum_name = $1 ORDER BY lfe.ordering ASC ", enum_name ) .fetch_optional(exec) .await? .map(|l| LoaderFieldEnum { id: LoaderFieldEnumId(l.id), enum_name: l.enum_name, ordering: l.ordering, hidable: l.hidable, }); redis .set_serialized_to_json( LOADER_FIELD_ENUMS_ID_NAMESPACE, enum_name, &result, None, ) .await?; Ok(result) } } impl LoaderFieldEnumValue { pub async fn list<'a, E>( loader_field_enum_id: LoaderFieldEnumId, exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { Ok(Self::list_many(&[loader_field_enum_id], exec, redis) .await? .into_iter() .next() .map(|x| x.1) .unwrap_or_default()) } pub async fn list_many_loader_fields<'a, E>( loader_fields: &[LoaderField], exec: E, redis: &RedisPool, ) -> Result>, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let get_enum_id = |x: &LoaderField| match x.field_type { LoaderFieldType::Enum(id) | LoaderFieldType::ArrayEnum(id) => { Some(id) } _ => None, }; let enum_ids = loader_fields .iter() .filter_map(get_enum_id) .collect::>(); let values = Self::list_many(&enum_ids, exec, redis) .await? .into_iter() .collect::>(); let mut res = HashMap::new(); for lf in loader_fields { if let Some(id) = get_enum_id(lf) { res.insert( lf.id, values.get(&id).unwrap_or(&Vec::new()).to_vec(), ); } } Ok(res) } pub async fn list_many<'a, E>( loader_field_enum_ids: &[LoaderFieldEnumId], exec: E, redis: &RedisPool, ) -> Result< HashMap>, DatabaseError, > where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let val = redis.get_cached_keys_raw( LOADER_FIELD_ENUM_VALUES_NAMESPACE, &loader_field_enum_ids.iter().map(|x| x.0).collect::>(), |loader_field_enum_ids| async move { let values = sqlx::query!( " SELECT id, enum_id, value, ordering, metadata, created FROM loader_field_enum_values WHERE enum_id = ANY($1) ORDER BY enum_id, ordering, created DESC ", &loader_field_enum_ids ) .fetch(exec) .try_fold(DashMap::new(), |acc: DashMap>, c| { let value = LoaderFieldEnumValue { id: LoaderFieldEnumValueId(c.id), enum_id: LoaderFieldEnumId(c.enum_id), value: c.value, ordering: c.ordering, created: c.created, metadata: c.metadata.unwrap_or_default(), }; acc.entry(c.enum_id) .or_default() .push(value); async move { Ok(acc) } }) .await?; Ok(values) }, ).await?; Ok(val .into_iter() .map(|x| (LoaderFieldEnumId(x.0), x.1)) .collect()) } // Matches filter against metadata of enum values pub async fn list_filter<'a, E>( loader_field_enum_id: LoaderFieldEnumId, filter: HashMap, exec: E, redis: &RedisPool, ) -> Result, DatabaseError> where E: sqlx::Executor<'a, Database = sqlx::Postgres>, { let result = Self::list(loader_field_enum_id, exec, redis) .await? .into_iter() .filter(|x| { let mut bool = true; for (key, value) in filter.iter() { if let Some(metadata_value) = x.metadata.get(key) { bool &= metadata_value == value; } else { bool = false; } } bool }) .collect(); Ok(result) } } impl VersionField { pub async fn insert_many( items: Vec, transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result<(), DatabaseError> { let mut query_version_fields = vec![]; for item in items { let base = QueryVersionField { version_id: item.version_id, field_id: item.field_id, int_value: None, enum_value: None, string_value: None, }; match item.value { VersionFieldValue::Integer(i) => { query_version_fields.push(base.clone().with_int_value(i)) } VersionFieldValue::Text(s) => { query_version_fields.push(base.clone().with_string_value(s)) } VersionFieldValue::Boolean(b) => query_version_fields .push(base.clone().with_int_value(if b { 1 } else { 0 })), VersionFieldValue::ArrayInteger(v) => { for i in v { query_version_fields .push(base.clone().with_int_value(i)); } } VersionFieldValue::ArrayText(v) => { for s in v { query_version_fields .push(base.clone().with_string_value(s)); } } VersionFieldValue::ArrayBoolean(v) => { for b in v { query_version_fields.push( base.clone().with_int_value(if b { 1 } else { 0 }), ); } } VersionFieldValue::Enum(_, v) => query_version_fields .push(base.clone().with_enum_value(v.id)), VersionFieldValue::ArrayEnum(_, v) => { for ev in v { query_version_fields .push(base.clone().with_enum_value(ev.id)); } } }; } let (field_ids, version_ids, int_values, enum_values, string_values): ( Vec<_>, Vec<_>, Vec<_>, Vec<_>, Vec<_>, ) = query_version_fields .iter() .map(|l| { ( l.field_id.0, l.version_id.0, l.int_value, l.enum_value.as_ref().map(|e| e.0), l.string_value.clone(), ) }) .multiunzip(); sqlx::query!( " INSERT INTO version_fields (field_id, version_id, int_value, string_value, enum_value) SELECT * FROM UNNEST($1::integer[], $2::bigint[], $3::integer[], $4::text[], $5::integer[]) ", &field_ids[..], &version_ids[..], &int_values[..] as &[Option], &string_values[..] as &[Option], &enum_values[..] as &[Option] ) .execute(&mut **transaction) .await?; Ok(()) } pub fn check_parse( version_id: VersionId, loader_field: LoaderField, value: serde_json::Value, enum_variants: Vec, ) -> Result { let value = VersionFieldValue::parse(&loader_field, value, enum_variants)?; // Ensure, if applicable, that the value is within the min/max bounds let countable = match &value { VersionFieldValue::Integer(i) => Some(*i), VersionFieldValue::ArrayInteger(v) => Some(v.len() as i32), VersionFieldValue::Text(_) => None, VersionFieldValue::ArrayText(v) => Some(v.len() as i32), VersionFieldValue::Boolean(_) => None, VersionFieldValue::ArrayBoolean(v) => Some(v.len() as i32), VersionFieldValue::Enum(_, _) => None, VersionFieldValue::ArrayEnum(_, v) => Some(v.len() as i32), }; if let Some(count) = countable { if let Some(min) = loader_field.min_val { if count < min { return Err(format!( "Provided value '{v}' for {field_name} is less than the minimum of {min}", v = serde_json::to_string(&value).unwrap_or_default(), field_name = loader_field.field, )); } } if let Some(max) = loader_field.max_val { if count > max { return Err(format!( "Provided value '{v}' for {field_name} is greater than the maximum of {max}", v = serde_json::to_string(&value).unwrap_or_default(), field_name = loader_field.field, )); } } } Ok(VersionField { version_id, field_id: loader_field.id, field_name: loader_field.field, value, }) } pub fn from_query_json( // A list of all version fields to extract data from query_version_field_combined: Vec, // A list of all loader fields to reference when extracting data // Note: any loader field in here that is not in query_version_field_combined will be still considered // (For example, game_versions in query_loader_fields but not in query_version_field_combined would produce game_versions: []) query_loader_fields: &[&QueryLoaderField], // enum values to reference when parsing enum values query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], // If true, will allow multiple values for a single singleton field, returning them as separate VersionFields // allow_many = true, multiple Bools => two VersionFields of Bool // allow_many = false, multiple Bools => error // multiple Arraybools => 1 VersionField of ArrayBool allow_many: bool, ) -> Vec { query_loader_fields .iter() .flat_map(|q| { let loader_field_type = match LoaderFieldType::build( &q.field_type, q.enum_type.map(|l| l.0), ) { Some(lft) => lft, None => return vec![], }; let loader_field = LoaderField { id: q.id, field: q.field.clone(), field_type: loader_field_type, optional: q.optional, min_val: q.min_val, max_val: q.max_val, }; // todo: avoid clone here? let version_fields = query_version_field_combined .iter() .filter(|qvf| qvf.field_id == q.id) .cloned() .collect::>(); if allow_many { VersionField::build_many( loader_field, version_fields, query_loader_field_enum_values, ) .unwrap_or_default() .into_iter() .unique() .collect_vec() } else { match VersionField::build( loader_field, version_fields, query_loader_field_enum_values, ) { Ok(vf) => vec![vf], Err(_) => vec![], } } }) .collect() } pub fn build( loader_field: LoaderField, query_version_fields: Vec, query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], ) -> Result { let (version_id, value) = VersionFieldValue::build( &loader_field.field_type, query_version_fields, query_loader_field_enum_values, )?; Ok(VersionField { version_id, field_id: loader_field.id, field_name: loader_field.field, value, }) } pub fn build_many( loader_field: LoaderField, query_version_fields: Vec, query_loader_field_enum_values: &[QueryLoaderFieldEnumValue], ) -> Result, DatabaseError> { let values = VersionFieldValue::build_many( &loader_field.field_type, query_version_fields, query_loader_field_enum_values, )?; Ok(values .into_iter() .map(|(version_id, value)| VersionField { version_id, field_id: loader_field.id, field_name: loader_field.field.clone(), value, }) .collect()) } } impl VersionFieldValue { // Build from user-submitted JSON data // value is the attempted value of the field, which will be tried to parse to the correct type // enum_array is the list of valid enum variants for the field, if it is an enum (see LoaderFieldEnumValue::list_many_loader_fields) pub fn parse( loader_field: &LoaderField, value: serde_json::Value, enum_array: Vec, ) -> Result { let field_name = &loader_field.field; let field_type = &loader_field.field_type; let error_value = value.clone(); let incorrect_type_error = |field_type: &str| { format!( "Provided value '{v}' for {field_name} could not be parsed to {field_type} ", v = serde_json::to_string(&error_value).unwrap_or_default() ) }; Ok(match field_type { LoaderFieldType::Integer => VersionFieldValue::Integer( serde_json::from_value(value) .map_err(|_| incorrect_type_error("integer"))?, ), LoaderFieldType::Text => VersionFieldValue::Text( value .as_str() .ok_or_else(|| incorrect_type_error("string"))? .to_string(), ), LoaderFieldType::Boolean => VersionFieldValue::Boolean( value .as_bool() .ok_or_else(|| incorrect_type_error("boolean"))?, ), LoaderFieldType::ArrayInteger => VersionFieldValue::ArrayInteger({ let array_values: Vec = serde_json::from_value(value) .map_err(|_| incorrect_type_error("array of integers"))?; array_values.into_iter().collect() }), LoaderFieldType::ArrayText => VersionFieldValue::ArrayText({ let array_values: Vec = serde_json::from_value(value) .map_err(|_| { incorrect_type_error("array of strings") })?; array_values.into_iter().collect() }), LoaderFieldType::ArrayBoolean => VersionFieldValue::ArrayBoolean({ let array_values: Vec = serde_json::from_value(value) .map_err(|_| incorrect_type_error("array of booleans"))?; array_values.into_iter().map(|v| v != 0).collect() }), LoaderFieldType::Enum(id) => VersionFieldValue::Enum(*id, { let enum_value = value .as_str() .ok_or_else(|| incorrect_type_error("enum"))?; if let Some(ev) = enum_array.into_iter().find(|v| v.value == enum_value) { ev } else { return Err(format!( "Provided value '{enum_value}' is not a valid variant for {field_name}" )); } }), LoaderFieldType::ArrayEnum(id) => { VersionFieldValue::ArrayEnum(*id, { let array_values: Vec = serde_json::from_value(value).map_err(|_| { incorrect_type_error("array of enums") })?; let mut enum_values = vec![]; for av in array_values { if let Some(ev) = enum_array.iter().find(|v| v.value == av) { enum_values.push(ev.clone()); } else { return Err(format!( "Provided value '{av}' is not a valid variant for {field_name}" )); } } enum_values }) } }) } // This will ensure that if multiple QueryVersionFields are provided, they can be combined into a single VersionFieldValue // of the appropriate type (ie: false, false, true -> ArrayBoolean([false, false, true])) (and not just Boolean) pub fn build( field_type: &LoaderFieldType, qvfs: Vec, qlfev: &[QueryLoaderFieldEnumValue], ) -> Result<(VersionId, VersionFieldValue), DatabaseError> { match field_type { LoaderFieldType::Integer | LoaderFieldType::Text | LoaderFieldType::Boolean | LoaderFieldType::Enum(_) => { let mut fields = Self::build_many(field_type, qvfs, qlfev)?; if fields.len() > 1 { return Err(DatabaseError::SchemaError(format!( "Multiple fields for field {}", field_type.to_str() ))); } fields.pop().ok_or_else(|| { DatabaseError::SchemaError(format!( "No version fields for field {}", field_type.to_str() )) }) } LoaderFieldType::ArrayInteger | LoaderFieldType::ArrayText | LoaderFieldType::ArrayBoolean | LoaderFieldType::ArrayEnum(_) => { let fields = Self::build_many(field_type, qvfs, qlfev)?; Ok(fields.into_iter().next().ok_or_else(|| { DatabaseError::SchemaError(format!( "No version fields for field {}", field_type.to_str() )) })?) } } } // Build from internal query data // This encapsulates redundant behavior in db query -> object conversions // This allows for multiple fields to be built at once. If there are multiple fields, // but the type only allows for a single field, then multiple VersionFieldValues will be returned // If there are multiple fields, and the type allows for multiple fields, then a single VersionFieldValue will be returned (array.len == 1) pub fn build_many( field_type: &LoaderFieldType, qvfs: Vec, qlfev: &[QueryLoaderFieldEnumValue], ) -> Result, DatabaseError> { let field_name = field_type.to_str(); let did_not_exist_error = |field_name: &str, desired_field: &str| { DatabaseError::SchemaError(format!( "Field name {} for field {} in does not exist", desired_field, field_name )) }; // Check errors- version_id must all be the same let version_id = qvfs .iter() .map(|qvf| qvf.version_id) .unique() .collect::>(); // If the field type is a non-array, then the reason for multiple version ids is that there are multiple versions being aggregated, and those version ids are contained within. // If the field type is an array, then the reason for multiple version ids is that there are multiple values for a single version // (or a greater aggregation between multiple arrays, in which case the per-field version is lost, so we just take the first one and use it for that) let version_id = version_id.into_iter().next().unwrap_or(VersionId(0)); let field_id = qvfs .iter() .map(|qvf| qvf.field_id) .unique() .collect::>(); if field_id.len() > 1 { return Err(DatabaseError::SchemaError(format!( "Multiple field ids for field {}", field_name ))); } let mut value = match field_type { // Singleton fields // If there are multiple, we assume multiple versions are being concatenated LoaderFieldType::Integer => qvfs .into_iter() .map(|qvf| { Ok(( qvf.version_id, VersionFieldValue::Integer(qvf.int_value.ok_or( did_not_exist_error(field_name, "int_value"), )?), )) }) .collect::, DatabaseError, >>()?, LoaderFieldType::Text => qvfs .into_iter() .map(|qvf| { Ok(( qvf.version_id, VersionFieldValue::Text(qvf.string_value.ok_or( did_not_exist_error(field_name, "string_value"), )?), )) }) .collect::, DatabaseError, >>()?, LoaderFieldType::Boolean => qvfs .into_iter() .map(|qvf| { Ok(( qvf.version_id, VersionFieldValue::Boolean( qvf.int_value.ok_or(did_not_exist_error( field_name, "int_value", ))? != 0, ), )) }) .collect::, DatabaseError, >>()?, LoaderFieldType::Enum(id) => qvfs .into_iter() .map(|qvf| { Ok(( qvf.version_id, VersionFieldValue::Enum(*id, { let enum_id = qvf.enum_value.ok_or( did_not_exist_error( field_name, "enum_value", ), )?; let lfev = qlfev .iter() .find(|x| x.id == enum_id) .ok_or(did_not_exist_error( field_name, "enum_value", ))?; LoaderFieldEnumValue { id: lfev.id, enum_id: lfev.enum_id, value: lfev.value.clone(), ordering: lfev.ordering, created: lfev.created, metadata: lfev .metadata .clone() .unwrap_or_default(), } }), )) }) .collect::, DatabaseError, >>()?, // Array fields // We concatenate into one array LoaderFieldType::ArrayInteger => vec![( version_id, VersionFieldValue::ArrayInteger( qvfs.into_iter() .map(|qvf| { qvf.int_value.ok_or(did_not_exist_error( field_name, "int_value", )) }) .collect::>()?, ), )], LoaderFieldType::ArrayText => vec![( version_id, VersionFieldValue::ArrayText( qvfs.into_iter() .map(|qvf| { qvf.string_value.ok_or(did_not_exist_error( field_name, "string_value", )) }) .collect::>()?, ), )], LoaderFieldType::ArrayBoolean => vec![( version_id, VersionFieldValue::ArrayBoolean( qvfs.into_iter() .map(|qvf| { Ok::( qvf.int_value.ok_or( did_not_exist_error( field_name, "int_value", ), )? != 0, ) }) .collect::>()?, ), )], LoaderFieldType::ArrayEnum(id) => vec![( version_id, VersionFieldValue::ArrayEnum( *id, qvfs.into_iter() .map(|qvf| { let enum_id = qvf.enum_value.ok_or( did_not_exist_error( field_name, "enum_value", ), )?; let lfev = qlfev .iter() .find(|x| x.id == enum_id) .ok_or(did_not_exist_error( field_name, "enum_value", ))?; Ok::<_, DatabaseError>(LoaderFieldEnumValue { id: lfev.id, enum_id: lfev.enum_id, value: lfev.value.clone(), ordering: lfev.ordering, created: lfev.created, metadata: lfev .metadata .clone() .unwrap_or_default(), }) }) .collect::>()?, ), )], }; // Sort arrayenums by ordering, then by created for (_, v) in value.iter_mut() { if let VersionFieldValue::ArrayEnum(_, v) = v { v.sort_by(|a, b| { a.ordering.cmp(&b.ordering).then(a.created.cmp(&b.created)) }); } } Ok(value) } // Serialize to internal value, such as for converting to user-facing JSON pub fn serialize_internal(&self) -> serde_json::Value { match self { VersionFieldValue::Integer(i) => { serde_json::Value::Number((*i).into()) } VersionFieldValue::Text(s) => serde_json::Value::String(s.clone()), VersionFieldValue::Boolean(b) => serde_json::Value::Bool(*b), VersionFieldValue::ArrayInteger(v) => serde_json::Value::Array( v.iter() .map(|i| serde_json::Value::Number((*i).into())) .collect(), ), VersionFieldValue::ArrayText(v) => serde_json::Value::Array( v.iter() .map(|s| serde_json::Value::String(s.clone())) .collect(), ), VersionFieldValue::ArrayBoolean(v) => serde_json::Value::Array( v.iter().map(|b| serde_json::Value::Bool(*b)).collect(), ), VersionFieldValue::Enum(_, v) => { serde_json::Value::String(v.value.clone()) } VersionFieldValue::ArrayEnum(_, v) => serde_json::Value::Array( v.iter() .map(|v| serde_json::Value::String(v.value.clone())) .collect(), ), } } // For conversion to an interanl string(s), such as for search facets, filtering, or direct hardcoding // No matter the type, it will be converted to a Vec, whre the non-array types will have a single element pub fn as_strings(&self) -> Vec { match self { VersionFieldValue::Integer(i) => vec![i.to_string()], VersionFieldValue::Text(s) => vec![s.clone()], VersionFieldValue::Boolean(b) => vec![b.to_string()], VersionFieldValue::ArrayInteger(v) => { v.iter().map(|i| i.to_string()).collect() } VersionFieldValue::ArrayText(v) => v.clone(), VersionFieldValue::ArrayBoolean(v) => { v.iter().map(|b| b.to_string()).collect() } VersionFieldValue::Enum(_, v) => vec![v.value.clone()], VersionFieldValue::ArrayEnum(_, v) => { v.iter().map(|v| v.value.clone()).collect() } } } pub fn contains_json_value(&self, value: &serde_json::Value) -> bool { match self { VersionFieldValue::Integer(i) => value.as_i64() == Some(*i as i64), VersionFieldValue::Text(s) => value.as_str() == Some(s), VersionFieldValue::Boolean(b) => value.as_bool() == Some(*b), VersionFieldValue::ArrayInteger(v) => value .as_i64() .map(|i| v.contains(&(i as i32))) .unwrap_or(false), VersionFieldValue::ArrayText(v) => value .as_str() .map(|s| v.contains(&s.to_string())) .unwrap_or(false), VersionFieldValue::ArrayBoolean(v) => { value.as_bool().map(|b| v.contains(&b)).unwrap_or(false) } VersionFieldValue::Enum(_, v) => value.as_str() == Some(&v.value), VersionFieldValue::ArrayEnum(_, v) => value .as_str() .map(|s| v.iter().any(|v| v.value == s)) .unwrap_or(false), } } }