names are now a property and can be chosen arbitrarily

master
Ondřej Hruška 3 years ago
parent fbd2a1ed75
commit e810724cd1
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 72
      Cargo.lock
  2. 1
      yopa-web/Cargo.toml
  3. 8
      yopa-web/resources/src/components/EditObjectForm.vue
  4. 13
      yopa-web/resources/src/components/NewObjectForm.vue
  5. 2
      yopa-web/resources/static/bundle.js
  6. 2
      yopa-web/resources/static/bundle.js.map
  7. 9
      yopa-web/resources/templates/models/model_create.html.tera
  8. 20
      yopa-web/resources/templates/models/model_update.html.tera
  9. 67
      yopa-web/resources/templates/models/property_create.html.tera
  10. 74
      yopa-web/resources/templates/models/property_update.html.tera
  11. 55
      yopa-web/resources/templates/models/relation_create.html.tera
  12. 35
      yopa-web/resources/templates/models/relation_update.html.tera
  13. 103
      yopa-web/src/routes/models/object.rs
  14. 9
      yopa-web/src/routes/models/property.rs
  15. 130
      yopa-web/src/routes/objects.rs
  16. 16
      yopa/src/cool.rs
  17. 126
      yopa/src/data.rs
  18. 3
      yopa/src/insert.rs
  19. 190
      yopa/src/lib.rs
  20. 5
      yopa/src/model.rs
  21. 8
      yopa/src/update.rs

72
Cargo.lock generated

@ -650,7 +650,7 @@ dependencies = [
"ansi_term", "ansi_term",
"atty", "atty",
"bitflags", "bitflags",
"strsim", "strsim 0.8.0",
"textwrap", "textwrap",
"unicode-width", "unicode-width",
"vec_map", "vec_map",
@ -736,6 +736,41 @@ dependencies = [
"cipher", "cipher",
] ]
[[package]]
name = "darling"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "11947000d710ff98138229f633039982f0fef2d9a3f546c21d610fee5f8631d5"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae53b4d9cc89c40314ccf2bf9e6ff1eb19c31e3434542445a41893dbf041aec2"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim 0.10.0",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9cd9ac4d50d023af5e710cae1501afb063efcd917bd3fc026e8ed6493cc9755"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "0.99.11" version = "0.99.11"
@ -1131,6 +1166,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.2.1" version = "0.2.1"
@ -1899,6 +1940,28 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_with"
version = "1.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b44be9227e214a0420707c9ca74c2d4991d9955bae9415a8f93f05cebf561be5"
dependencies = [
"serde",
"serde_with_macros",
]
[[package]]
name = "serde_with_macros"
version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e48b35457e9d855d3dc05ef32a73e0df1e2c0fd72c38796a4ee909160c8eeec2"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "sha-1" name = "sha-1"
version = "0.8.2" version = "0.8.2"
@ -2059,6 +2122,12 @@ version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.0" version = "2.4.0"
@ -2670,6 +2739,7 @@ dependencies = [
"rand 0.8.3", "rand 0.8.3",
"serde", "serde",
"serde_json", "serde_json",
"serde_with",
"simple-logging", "simple-logging",
"tera", "tera",
"thiserror", "thiserror",

@ -27,6 +27,7 @@ json_dotpath = "1.0.3"
anyhow = "1.0.38" anyhow = "1.0.38"
thiserror = "1.0.24" thiserror = "1.0.24"
clap = "2" clap = "2"
serde_with = "1.6.4"
tokio = { version="0.2.6", features=["full"] } tokio = { version="0.2.6", features=["full"] }

@ -86,7 +86,6 @@ export default {
return { return {
model: this.object.model, // string is fine model: this.object.model, // string is fine
id: this.object.id, id: this.object.id,
name: this.name,
values, values,
relations, relations,
}; };
@ -140,13 +139,6 @@ export default {
<p><input type="button" value="Save" @click="trySave"></p> <p><input type="button" value="Save" @click="trySave"></p>
<table> <table>
<tr>
<th><label for="field-name">Name</label></th>
<td>
<input type="text" id="field-name" v-model="name">
</td>
</tr>
<edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property> <edit-property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></edit-property>
</table> </table>

@ -39,17 +39,12 @@ export default {
haveRelations: !isEmpty(relations), haveRelations: !isEmpty(relations),
model_names, model_names,
values, values,
name: '',
relationRefs: [], relationRefs: [],
} }
}, },
methods: { methods: {
/** Get values in the raw format without grouping */ /** Get values in the raw format without grouping */
collectData() { collectData() {
if (isEmpty(this.name)) {
throw new Error("Name is required");
}
let values = []; let values = [];
forEach(objCopy(this.values), (vv, prop_model_id) => { forEach(objCopy(this.values), (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
@ -73,7 +68,6 @@ export default {
return { return {
model: this.model.id, model: this.model.id,
name: this.name,
values, values,
relations, relations,
}; };
@ -127,13 +121,6 @@ export default {
<p><input type="button" value="Save" @click="trySave"></p> <p><input type="button" value="Save" @click="trySave"></p>
<table> <table>
<tr>
<th><label for="field-name">Name</label></th>
<td>
<input type="text" id="field-name" v-model="name">
</td>
</tr>
<property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></property> <property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></property>
</table> </table>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -13,8 +13,13 @@ Define object
<h1>Define new object model</h1> <h1>Define new object model</h1>
<form action="/model/object/create" method="POST"> <form action="/model/object/create" method="POST">
<label for="name">Name:</label> <table>
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br> <tr>
<th><label for="name">Name:</label></th>
<td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off">
</td>
</tr>
</table>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -13,8 +13,24 @@ Edit object model
<h1>Edit object model {{ model.name }}</h1> <h1>Edit object model {{ model.name }}</h1>
<form action="/model/object/update/{{ model.id }}" method="POST"> <form action="/model/object/update/{{ model.id }}" method="POST">
<label for="name">Name:</label> <table>
<input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off"><br> <tr>
<th><label for="name">Name:</label></th>
<td><input type="text" id="name" name="name" value="{{ model.name }}" autocomplete="off">
</td>
</tr>
<tr>
<th><label for="name_property">Name property:</label></th>
<td>
<select name="name_property" id="name_property" autocomplete="off">
<option value=""></option>
{% for p in properties %}
<option value="{{ p.id }}" {{selected(val=old.name_property, opt=p.id)}}>{{ p.name }}</option>
{% endfor %}
</select>
</td>
</tr>
</table>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -16,28 +16,55 @@ Define property
<form action="/model/property/create" method="POST"> <form action="/model/property/create" method="POST">
<input type="hidden" name="object" value="{{object.id}}"> <input type="hidden" name="object" value="{{object.id}}">
<label for="name">Name:</label> <table>
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br> <tr>
<th><label for="name">Name:</label></th>
<label for="optional">Optional:</label> <td><input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"></td>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off"> </tr>
<br> <tr>
<th><label for="unique">Unique:</label></th>
<label for="multiple">Multiple:</label> <td><input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=old.unique)}} autocomplete="off"></td>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br> </tr>
<tr>
<label for="data_type">Type:</label> <th><label for="optional">Optional:</label></th>
<select name="data_type" id="data_type" autocomplete="off"> <td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off"></td>
<option value="String" {{selected(opt="String",val=old.data_type)}}>String</option> </tr>
<option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option> <tr>
<option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option> <th><label for="multiple">Multiple:</label></th>
<option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option> <td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"></td>
</select><br> </tr>
<tr>
<label for="default">Default:</label> <th><label for="data_type">Type:</label></th>
<input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"><br> <td>
<select name="data_type" id="data_type" autocomplete="off">
<option value="String" {{selected(opt="String",val=old.data_type)}}>String</option>
<option value="Integer" {{selected(opt="Integer",val=old.data_type)}}>Integer</option>
<option value="Decimal" {{selected(opt="Decimal",val=old.data_type)}}>Decimal</option>
<option value="Boolean" {{selected(opt="Boolean",val=old.data_type)}}>Boolean</option>
</select>
</td>
</tr>
<tr>
<th><label for="default">Default:</label></th>
<td><input type="text" id="default" name="default" value="{{old.default}}" autocomplete="off"></td>
</tr>
</table>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>
<script>
(function () {
// multiple and unique are XORed. This is also enforced server-side
let multiple = document.getElementById('multiple');
let unique = document.getElementById('unique');
unique.addEventListener('input', function () {
multiple.checked &= !unique.checked;
})
multiple.addEventListener('input', function () {
unique.checked &= !multiple.checked;
})
})();
</script>
{%- endblock %} {%- endblock %}

@ -13,28 +13,64 @@ Edit property
<h1>Edit property {{model.name}}</h1> <h1>Edit property {{model.name}}</h1>
<form action="/model/property/update/{{model.id}}" method="POST"> <form action="/model/property/update/{{model.id}}" method="POST">
<label for="name">Name:</label>
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br>
<label for="optional">Optional:</label> <table>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off"> <tr>
<br> <th><label for="name">Name:</label></th>
<td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"></td>
<label for="multiple">Multiple:</label> </tr>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br> <tr>
<th><label for="unique">Unique:</label></th>
<label for="data_type">Type:</label> <td>
<select name="data_type" id="data_type" autocomplete="off"> <input type="checkbox" name="unique" id="unique" value="true" {{opt(checked=model.unique)}} autocomplete="off">
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option> </td>
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option> </tr>
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option> <tr>
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option> <th><label for="optional">Optional:</label></th>
</select><br> <td>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
<label for="default">Default:</label> </td>
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off"><br> </tr>
<tr>
<th><label for="multiple">Multiple:</label></th>
<td>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
</td>
</tr>
<tr>
<th><label for="data_type">Type:</label></th>
<td>
<select name="data_type" id="data_type" autocomplete="off">
<option value="String" {{selected(val=model.data_type, opt="String")}}>String</option>
<option value="Integer" {{selected(val=model.data_type, opt="Integer")}}>Integer</option>
<option value="Decimal" {{selected(val=model.data_type, opt="Decimal")}}>Decimal</option>
<option value="Boolean" {{selected(val=model.data_type, opt="Boolean")}}>Boolean</option>
</select>
</td>
</tr>
<tr>
<th><label for="default">Default:</label></th>
<td>
<input type="text" id="default" name="default" value="{{model.default | print_typed_value}}" autocomplete="off">
</td>
</tr>
</table>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>
<script>
(function () {
// multiple and unique are XORed. This is also enforced server-side
let multiple = document.getElementById('multiple');
let unique = document.getElementById('unique');
unique.addEventListener('input', function () {
multiple.checked &= !unique.checked;
})
multiple.addEventListener('input', function () {
unique.checked &= !multiple.checked;
})
})();
</script>
{%- endblock %} {%- endblock %}

@ -15,25 +15,42 @@ Define relation
<form action="/model/relation/create" method="POST"> <form action="/model/relation/create" method="POST">
<input type="hidden" name="object" value="{{object.id}}"> <input type="hidden" name="object" value="{{object.id}}">
<label for="name">Name:</label> <table>
<input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br> <tr>
<th><label for="name">Name:</label></th>
<label for="reciprocal_name">Reciprocal name:</label> <td>
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off"><br> <input type="text" id="name" name="name" value="{{old.name}}" autocomplete="off"><br>
</td>
<label for="optional">Optional:</label> </tr>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off"> <tr>
<br> <th><label for="reciprocal_name">Reciprocal name:</label></th>
<td>
<label for="multiple">Multiple:</label> <input type="text" id="reciprocal_name" name="reciprocal_name" value="{{old.reciprocal_name}}" autocomplete="off">
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off"><br> </td>
</tr>
<label for="related">Related object:</label> <tr>
<select name="related" id="related" autocomplete="off"> <th><label for="optional">Optional:</label></th>
{% for m in models %} <td>
<option value="{{ m.id }}" {{selected(val=old.related, opt=m.id)}}>{{ m.name }}</option> <input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=old.optional)}} autocomplete="off">
{% endfor %} </td>
</select><br> </tr>
<tr>
<th><label for="multiple">Multiple:</label></th>
<td>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=old.multiple)}} autocomplete="off">
</td>
</tr>
<tr>
<th><label for="related">Related object:</label></th>
<td>
<select name="related" id="related" autocomplete="off">
{% for m in models %}
<option value="{{ m.id }}" {{selected(val=old.related, opt=m.id)}}>{{ m.name }}</option>
{% endfor %}
</select>
</td>
</tr>
</table>
<input type="submit" value="Save"> <input type="submit" value="Save">
</form> </form>

@ -13,18 +13,29 @@ Edit relation
<h1>Edit relation model "{{model.name}}"</h1> <h1>Edit relation model "{{model.name}}"</h1>
<form action="/model/relation/update/{{model.id}}" method="POST"> <form action="/model/relation/update/{{model.id}}" method="POST">
<label for="name">Name:</label> <table>
<input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off"><br> <tr>
<th><label for="name">Name:</label></th>
<label for="reciprocal_name">Reciprocal name:</label> <td><input type="text" id="name" name="name" value="{{model.name}}" autocomplete="off">
<input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off"><br> </td>
</tr>
<label for="optional">Optional:</label> <tr>
<input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off"> <th><label for="reciprocal_name">Reciprocal name:</label></th>
<br> <td><input type="text" id="reciprocal_name" name="reciprocal_name" value="{{model.reciprocal_name}}" autocomplete="off">
</td>
<label for="multiple">Multiple:</label> </tr>
<input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off"><br> <tr>
<th><label for="optional">Optional:</label>
</th>
<td><input type="checkbox" name="optional" id="optional" value="true" {{opt(checked=model.optional)}} autocomplete="off">
</td>
</tr>
<tr>
<th><label for="multiple">Multiple:</label></th>
<td><input type="checkbox" name="multiple" id="multiple" value="true" {{opt(checked=model.multiple)}} autocomplete="off">
</td>
</tr>
</table>
<p>The related object cannot be changed. Create a new relation if needed.</p> <p>The related object cannot be changed. Create a new relation if needed.</p>

@ -10,6 +10,7 @@ use crate::session_ext::SessionExt;
use crate::tera_ext::TeraExt; use crate::tera_ext::TeraExt;
use crate::utils::{redirect, StorageErrorIntoResponseError}; use crate::utils::{redirect, StorageErrorIntoResponseError};
use crate::TERA; use crate::TERA;
use itertools::Itertools;
#[derive(Serialize, Debug)] #[derive(Serialize, Debug)]
pub(crate) struct ObjectModelDisplay<'a> { pub(crate) struct ObjectModelDisplay<'a> {
@ -38,6 +39,10 @@ pub(crate) async fn create_form(session: Session) -> actix_web::Result<impl Resp
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub(crate) struct ObjectModelForm { pub(crate) struct ObjectModelForm {
pub name: String, pub name: String,
#[serde(default)]
// #[serde(with="serde_with::rust::default_on_error")] // This is because "" can be selected
#[serde(with="my_string_empty_as_none")]
pub name_property: Option<ID>,
} }
#[post("/model/object/create")] #[post("/model/object/create")]
@ -51,6 +56,7 @@ pub(crate) async fn create(
match wg.define_object(ObjectModel { match wg.define_object(ObjectModel {
id: Default::default(), id: Default::default(),
name: form.name.clone(), name: form.name.clone(),
name_property: form.name_property
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
@ -84,12 +90,22 @@ pub(crate) async fn update_form(
// Re-fill old values // Re-fill old values
if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") { if let Ok(Some(form)) = session.take::<ObjectModelForm>("old") {
let mut model = model.clone(); let mut model = model.clone();
context.insert("old", &form);
model.name = form.name; model.name = form.name;
model.name_property = form.name_property;
context.insert("model", &model); context.insert("model", &model);
} else { } else {
context.insert("model", model); context.insert("model", model);
context.insert("old", &ObjectModelForm {
name: model.name.to_string(),
name_property: model.name_property
});
} }
let properties = rg.get_property_models_for_parent(*model_id).collect_vec();
context.insert("properties", &properties);
TERA.build_response("models/model_update", &context) TERA.build_response("models/model_update", &context)
} }
@ -107,6 +123,7 @@ pub(crate) async fn update(
match wg.update_object_model(ObjectModel { match wg.update_object_model(ObjectModel {
id, id,
name: form.name.clone(), name: form.name.clone(),
name_property: form.name_property,
}) { }) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
@ -144,3 +161,89 @@ pub(crate) async fn delete(
} }
} }
} }
pub mod my_string_empty_as_none {
use serde::{Deserializer, Serializer, Serialize};
use std::str::FromStr;
use std::fmt::{Display, Write};
use std::marker::PhantomData;
use serde::de::{Visitor, Error};
use std::fmt;
use yopa::ID;
// FIXME largely copied from serde_with
/// Deserialize an `Option<T>` from a string using `FromStr`
pub fn deserialize<'de, D, S>(deserializer: D) -> Result<Option<S>, D::Error>
where
D: Deserializer<'de>,
S: FromStr,
S::Err: Display,
{
struct OptionStringEmptyNone<S>(PhantomData<S>);
impl<'de, S> Visitor<'de> for OptionStringEmptyNone<S>
where
S: FromStr,
S::Err: Display,
{
type Value = Option<S>;
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("any string")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: Error,
{
match value {
"" => Ok(None),
v => S::from_str(v).map(Some).map_err(Error::custom),
}
}
fn visit_string<E>(self, value: String) -> Result<Self::Value, E>
where
E: Error,
{
match &*value {
"" => Ok(None),
v => S::from_str(v).map(Some).map_err(Error::custom),
}
}
// TODO remove?
fn visit_u64<E>(self, v: u64) -> Result<Self::Value, E>
where
E: Error,
{
self.visit_str(&v.to_string())
}
// handles the `null` case
fn visit_unit<E>(self) -> Result<Self::Value, E>
where
E: Error,
{
Ok(None)
}
}
deserializer.deserialize_any(OptionStringEmptyNone(PhantomData))
}
/// Serialize a string from `Option<T>` using `AsRef<str>` or using the empty string if `None`.
pub fn serialize<S>(option: &Option<ID>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
option.serialize(serializer)
// if let Some(value) = option {
// value.serialize(serializer)
// } else {
// "".serialize(serializer)
// }
}
}

@ -28,11 +28,13 @@ pub(crate) async fn create_form(
} else { } else {
context.insert( context.insert(
"old", "old",
// This is the defaults for the form
&PropertyModelCreateForm { &PropertyModelCreateForm {
object: Default::default(), object: Default::default(),
name: "".to_string(), name: "".to_string(),
optional: true, optional: true,
multiple: false, multiple: false,
unique: false,
data_type: DataType::String, data_type: DataType::String,
default: "".to_string(), default: "".to_string(),
}, },
@ -70,6 +72,8 @@ pub(crate) struct PropertyModelCreateForm {
pub optional: bool, pub optional: bool,
#[serde(default)] #[serde(default)]
pub multiple: bool, pub multiple: bool,
#[serde(default)]
pub unique: bool,
pub data_type: DataType, pub data_type: DataType,
/// Default value to be parsed to the data type /// Default value to be parsed to the data type
/// May be unused if empty and optional /// May be unused if empty and optional
@ -126,6 +130,7 @@ pub(crate) async fn create(
let optional = form.optional; let optional = form.optional;
let multiple = form.multiple; let multiple = form.multiple;
let unique = form.unique;
let default = match parse_default(form.data_type, form.default.clone()) { let default = match parse_default(form.data_type, form.default.clone()) {
Ok(def) => def, Ok(def) => def,
@ -143,6 +148,7 @@ pub(crate) async fn create(
name: form.name.clone(), name: form.name.clone(),
optional, optional,
multiple, multiple,
unique,
data_type: form.data_type, data_type: form.data_type,
default, default,
}) { }) {
@ -190,6 +196,8 @@ pub(crate) struct PropertyModelEditForm {
pub optional: bool, pub optional: bool,
#[serde(default)] #[serde(default)]
pub multiple: bool, pub multiple: bool,
#[serde(default)]
pub unique: bool,
pub data_type: DataType, pub data_type: DataType,
/// Default value to be parsed to the data type /// Default value to be parsed to the data type
/// May be unused if empty and optional /// May be unused if empty and optional
@ -253,6 +261,7 @@ pub(crate) async fn update(
name: form.name.clone(), name: form.name.clone(),
optional: form.optional, optional: form.optional,
multiple: form.multiple, multiple: form.multiple,
unique: form.unique,
data_type: form.data_type, data_type: form.data_type,
default, default,
}) { }) {

@ -1,22 +1,24 @@
use crate::session_ext::SessionExt; use std::borrow::{Borrow, Cow};
use actix_session::Session; use std::collections::HashMap;
use actix_web::{web, HttpResponse, Responder};
use crate::tera_ext::TeraExt;
use crate::utils::{redirect, StorageErrorIntoResponseError};
use crate::TERA;
use serde::Serialize;
use yopa::data::Object;
use yopa::insert::InsertObj;
use yopa::{data, model, Storage, ID};
use actix_session::Session;
use actix_web::{HttpResponse, Responder, web};
use heck::TitleCase; use heck::TitleCase;
use itertools::Itertools; use itertools::Itertools;
use json_dotpath::DotPaths; use json_dotpath::DotPaths;
use std::collections::HashMap; use serde::Serialize;
use yopa::{data, ID, model, Storage};
use yopa::data::Object;
use yopa::insert::InsertObj;
use yopa::model::{ObjectModel, PropertyModel, RelationModel}; use yopa::model::{ObjectModel, PropertyModel, RelationModel};
use yopa::update::UpdateObj; use yopa::update::UpdateObj;
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::utils::{redirect, StorageErrorIntoResponseError};
// we only need references here, Context serializes everything to Value. // we only need references here, Context serializes everything to Value.
// cloning would be a waste of cycles // cloning would be a waste of cycles
@ -27,11 +29,18 @@ pub struct Schema<'a> {
pub prop_models: Vec<&'a model::PropertyModel>, pub prop_models: Vec<&'a model::PropertyModel>,
} }
#[derive(Debug, Clone, Serialize)]
pub struct ObjectDisplay<'a> {
pub id: ID,
pub model: ID,
pub name: Cow<'a, str>,
}
#[derive(Serialize, Debug, Clone)] #[derive(Serialize, Debug, Clone)]
pub struct ObjectCreateData<'a> { pub struct ObjectCreateData<'a> {
pub model_id: ID, pub model_id: ID,
pub schema: Schema<'a>, pub schema: Schema<'a>,
pub objects: Vec<&'a Object>, pub objects: Vec<ObjectDisplay<'a>>,
} }
#[get("/object/create/{model_id}")] #[get("/object/create/{model_id}")]
@ -76,16 +85,25 @@ fn prepare_object_create_data(rg: &Storage, model_id: ID) -> actix_web::Result<O
related_ids.sort(); related_ids.sort();
related_ids.dedup(); related_ids.dedup();
let mut models_to_fetch = vec![model_id];
models_to_fetch.extend(&related_ids);
Ok(ObjectCreateData { Ok(ObjectCreateData {
model_id: model.id, model_id: model.id,
schema: Schema { schema: Schema {
obj_models: rg.get_object_models().collect(), // TODO get only the ones that matter here obj_models: rg.get_object_models_by_ids(models_to_fetch).collect(), // TODO get only the ones that matter here
rel_models: relations, rel_models: relations,
prop_models: rg prop_models: rg
.get_property_models_for_parents(prop_object_ids) .get_property_models_for_parents(prop_object_ids)
.collect(), .collect(),
}, },
objects: rg.get_objects_of_types(related_ids).collect(), objects: rg.get_objects_of_types(related_ids).map(|o| {
ObjectDisplay {
id: o.id,
model: o.model,
name: rg.get_object_name(o),
}
}).collect(),
}) })
} }
@ -97,22 +115,17 @@ pub(crate) async fn create(
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
warn!("{:?}", form); warn!("{:?}", form);
// let des : InsertObj = serde_json::from_value(form.into_inner()).unwrap();
//
// Ok(HttpResponse::Ok().finish())
//
let mut wg = store.write().await; let mut wg = store.write().await;
let form = form.into_inner(); let form = form.into_inner();
let name = form.name.clone();
let model_name = wg.get_model_name(form.model).to_owned().to_title_case(); let model_name = wg.get_model_name(form.model).to_owned().to_title_case();
match wg.insert_object(form) { match wg.insert_object(form) {
Ok(_id) => { Ok(id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
let obj_name = wg.get_object_name_by_id(id);
debug!("Object created, redirecting to root"); debug!("Object created, redirecting to root");
session.flash_success(format!("{} \"{}\" created.", model_name, name)); session.flash_success(format!("{} \"{}\" created.", model_name, obj_name));
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
Err(e) => { Err(e) => {
@ -125,7 +138,7 @@ pub(crate) async fn create(
#[derive(Debug, Serialize, Clone)] #[derive(Debug, Serialize, Clone)]
struct ModelWithObjects<'a> { struct ModelWithObjects<'a> {
model: &'a ObjectModel, model: &'a ObjectModel,
objects: Vec<&'a Object>, objects: Vec<ObjectDisplay<'a>>,
} }
#[get("/objects")] #[get("/objects")]
@ -148,8 +161,16 @@ pub(crate) async fn list_inner(
.get_object_models() .get_object_models()
.sorted_by_key(|m| &m.name) .sorted_by_key(|m| &m.name)
.map(|model| { .map(|model| {
let mut objects = objects_by_model.remove(&model.id).unwrap_or_default(); let objects = objects_by_model.remove(&model.id).unwrap_or_default();
objects.sort_by_key(|o| &o.name); let mut objects = objects.into_iter().map(|o| {
ObjectDisplay {
id: o.id,
model: o.model,
name: rg.get_object_name(o), // TODO optimize
}
}).collect_vec();
objects.sort_by(|a, b| a.name.cmp(&b.name));
ModelWithObjects { model, objects } ModelWithObjects { model, objects }
}) })
.collect(); .collect();
@ -176,7 +197,8 @@ struct RelationView<'a> {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct RelationInstanceView<'a> { struct RelationInstanceView<'a> {
related: &'a Object, related: ObjectDisplay<'a>,
related_name: Cow<'a, str>,
properties: Vec<PropertyView<'a>>, properties: Vec<PropertyView<'a>>,
} }
@ -201,7 +223,11 @@ pub(crate) async fn detail(
.get_object_model(object.model) .get_object_model(object.model)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?; .ok_or_else(|| actix_web::error::ErrorNotFound("No such model"))?;
context.insert("object", object); context.insert("object", &ObjectDisplay {
id: object_id,
model: object.model,
name: rg.get_object_name(object),
});
context.insert("model", model); context.insert("model", model);
context.insert("kind", &rg.get_model_name(object.model)); context.insert("kind", &rg.get_model_name(object.model));
@ -270,13 +296,20 @@ pub(crate) async fn detail(
view_rel_properties.sort_by_key(|p| &p.model.name); view_rel_properties.sort_by_key(|p| &p.model.name);
let related_name = rg.get_object_name(related_obj);
instances.push(RelationInstanceView { instances.push(RelationInstanceView {
related: related_obj, related: ObjectDisplay {
id: related_obj.id,
model: related_obj.model,
name: rg.get_object_name(related_obj),
},
related_name,
properties: view_rel_properties, properties: view_rel_properties,
}) })
} }
instances.sort_by_key(|r| &r.related.name); instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
relation_views.push(RelationView { relation_views.push(RelationView {
model: rg.get_relation_model(model_id).unwrap(), model: rg.get_relation_model(model_id).unwrap(),
@ -322,13 +355,21 @@ pub(crate) async fn detail(
view_rel_properties.sort_by_key(|p| &p.model.name); view_rel_properties.sort_by_key(|p| &p.model.name);
let related_name = rg.get_object_name(related_obj);
instances.push(RelationInstanceView { instances.push(RelationInstanceView {
related: related_obj, related: ObjectDisplay {
id: related_obj.id,
model: related_obj.model,
name: rg.get_object_name(related_obj),
},
related_name,
properties: view_rel_properties, properties: view_rel_properties,
}) })
} }
instances.sort_by_key(|r| &r.related.name); // sort_by_key is not possible because rust is still stupid about lifetimes
instances.sort_by(|a, b| a.related_name.cmp(&b.related_name));
relation_views.push(RelationView { relation_views.push(RelationView {
model: rg.get_relation_model(model_id).unwrap(), model: rg.get_relation_model(model_id).unwrap(),
@ -349,7 +390,7 @@ pub(crate) async fn detail(
struct EnrichedObject<'a> { struct EnrichedObject<'a> {
id: ID, id: ID,
model: ID, model: ID,
name: String, name: Cow<'a, str>,
values: HashMap< values: HashMap<
String, /* ID but as string so serde will stop exploding */ String, /* ID but as string so serde will stop exploding */
Vec<&'a data::Value>, Vec<&'a data::Value>,
@ -387,7 +428,11 @@ pub(crate) async fn update_form(
// maybe its useful,idk // maybe its useful,idk
context.insert("model", &model); context.insert("model", &model);
context.insert("object", &object); context.insert("object", &ObjectDisplay {
id: object.id,
model: object.model,
name: rg.get_object_name(object),
});
let create_data = prepare_object_create_data(&rg, model.id)?; let create_data = prepare_object_create_data(&rg, model.id)?;
@ -480,7 +525,7 @@ pub(crate) async fn update_form(
let object = EnrichedObject { let object = EnrichedObject {
id: object.id, id: object.id,
model: object.model, model: object.model,
name: object.name.clone(), name: rg.get_object_name_by_id(object.id),
values: value_map, values: value_map,
relations: relation_map, relations: relation_map,
}; };
@ -494,26 +539,26 @@ pub(crate) async fn update_form(
#[post("/object/update/{id}")] #[post("/object/update/{id}")]
pub(crate) async fn update( pub(crate) async fn update(
_id: web::Path<ID>, id: web::Path<ID>,
form: web::Json<UpdateObj>, form: web::Json<UpdateObj>,
store: crate::YopaStoreWrapper, store: crate::YopaStoreWrapper,
session: Session, session: Session,
) -> actix_web::Result<HttpResponse> { ) -> actix_web::Result<HttpResponse> {
let mut wg = store.write().await; let mut wg = store.write().await;
let form = form.into_inner(); let form = form.into_inner();
let name = form.name.clone();
let object = wg let object = wg
.get_object(form.id) .get_object(*id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?; .ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
let model_name = wg.get_model_name(object.model).to_owned().to_title_case(); let model_name = wg.get_model_name(object.model).to_owned().to_title_case();
let id = object.id;
match wg.update_object(form) { match wg.update_object(form) {
Ok(_id) => { Ok(_id) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
debug!("Object created, redirecting to root"); debug!("Object created, redirecting to root");
session.flash_success(format!("{} \"{}\" updated.", model_name, name)); session.flash_success(format!("{} \"{}\" updated.", model_name, wg.get_object_name_by_id(id)));
Ok(HttpResponse::Ok().finish()) Ok(HttpResponse::Ok().finish())
} }
Err(e) => { Err(e) => {
@ -530,11 +575,14 @@ pub(crate) async fn delete(
session: Session, session: Session,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let mut wg = store.write().await; let mut wg = store.write().await;
let name = wg.get_object_name_by_id(*id).to_string();
match wg.delete_object(*id) { match wg.delete_object(*id) {
Ok(obj) => { Ok(_obj) => {
wg.persist().err_to_500()?; wg.persist().err_to_500()?;
debug!("Object deleted, redirecting to root"); debug!("Object deleted, redirecting to root");
session.flash_success(format!("Object \"{}\" deleted.", obj.name)); session.flash_success(format!("Object \"{}\" deleted.", name));
redirect("/") redirect("/")
} }
Err(e) => { Err(e) => {

@ -39,3 +39,19 @@ impl<K, V> KVVecToKeysOrValues<K, V> for Vec<(K, V)> {
self.into_iter().map(|(_k, v)| v).collect() self.into_iter().map(|(_k, v)| v).collect()
} }
} }
pub(crate) trait IsNoneOrElse<T> : Sized {
//noinspection RsSelfConvention
fn is_none_or_else(&self, test : impl FnOnce(&T) -> bool) -> bool;
}
impl<T> IsNoneOrElse<T> for Option<T> {
fn is_none_or_else(&self, test: impl FnOnce(&T) -> bool) -> bool {
match self {
None => true,
Some(value) => {
test(value)
}
}
}
}

@ -1,6 +1,6 @@
//! Data value structs //! Data value structs
use std::borrow::Cow; use std::borrow::{Cow, Borrow};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -35,6 +35,16 @@ impl Display for TypedValue {
} }
impl TypedValue { impl TypedValue {
/// To Cow<str>
pub fn to_cow(&self) -> Cow<str> {
match self {
TypedValue::String(v) => Cow::Borrowed(v.borrow()),
TypedValue::Integer(v) => v.to_string().into(),
TypedValue::Decimal(v) => v.to_string().into(),
TypedValue::Boolean(v) => if *v { "True" } else { "False" }.into(),
}
}
/// Try ot cast to another type. On error, the original value is returned as Err. /// Try ot cast to another type. On error, the original value is returned as Err.
pub fn cast_to(self, ty: DataType) -> Result<TypedValue, TypedValue> { pub fn cast_to(self, ty: DataType) -> Result<TypedValue, TypedValue> {
match (self, ty) { match (self, ty) {
@ -83,6 +93,62 @@ impl TypedValue {
} }
} }
/// Instance of an object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Object {
/// PK
#[serde(default)]
pub id: ID,
/// Object template ID
pub model: ID,
}
/// Relation between two objects
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation {
/// PK
#[serde(default)]
pub id: ID,
/// Source object ID
pub object: ID,
/// Relation template ID
pub model: ID,
/// Related object ID
pub related: ID,
}
/// Value attached to an object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Value {
/// PK
#[serde(default)]
pub id: ID,
/// Owning object ID
pub object: ID,
/// Property template ID
pub model: ID,
/// Property value
pub value: TypedValue,
}
impl HaveId for Object {
fn get_id(&self) -> ID {
self.id
}
}
impl HaveId for Relation {
fn get_id(&self) -> ID {
self.id
}
}
impl HaveId for Value {
fn get_id(&self) -> ID {
self.id
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::data::TypedValue; use crate::data::TypedValue;
@ -300,61 +366,3 @@ mod tests {
); );
} }
} }
/// Instance of an object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Object {
/// PK
#[serde(default)]
pub id: ID,
/// Object template ID
pub model: ID,
/// Model name, mainly shown in lists
pub name: String,
}
/// Relation between two objects
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Relation {
/// PK
#[serde(default)]
pub id: ID,
/// Source object ID
pub object: ID,
/// Relation template ID
pub model: ID,
/// Related object ID
pub related: ID,
}
/// Value attached to an object
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Value {
/// PK
#[serde(default)]
pub id: ID,
/// Owning object ID
pub object: ID,
/// Property template ID
pub model: ID,
/// Property value
pub value: TypedValue,
}
impl HaveId for Object {
fn get_id(&self) -> ID {
self.id
}
}
impl HaveId for Relation {
fn get_id(&self) -> ID {
self.id
}
}
impl HaveId for Value {
fn get_id(&self) -> ID {
self.id
}
}

@ -43,7 +43,6 @@ impl InsertRel {
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertObj { pub struct InsertObj {
pub model: ID, pub model: ID,
pub name: String,
pub values: Vec<InsertValue>, pub values: Vec<InsertValue>,
pub relations: Vec<InsertRel>, pub relations: Vec<InsertRel>,
} }
@ -51,13 +50,11 @@ pub struct InsertObj {
impl InsertObj { impl InsertObj {
pub fn new( pub fn new(
model_id: ID, model_id: ID,
name: String,
values: Vec<InsertValue>, values: Vec<InsertValue>,
relations: Vec<InsertRel>, relations: Vec<InsertRel>,
) -> Self { ) -> Self {
Self { Self {
model: model_id, model: model_id,
name,
values, values,
relations, relations,
} }

@ -6,7 +6,7 @@ extern crate log;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::HashMap;
use std::fs::OpenOptions; use std::fs::OpenOptions;
use std::io::{BufReader, BufWriter}; use std::io::{BufReader, BufWriter, Read, Write};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use itertools::Itertools; use itertools::Itertools;
@ -23,6 +23,8 @@ use model::ObjectModel;
use crate::model::{PropertyModel, RelationModel}; use crate::model::{PropertyModel, RelationModel};
use crate::update::{UpdateObj, UpsertValue}; use crate::update::{UpdateObj, UpsertValue};
use crate::cool::IsNoneOrElse;
use crate::data::{Object, Value};
mod cool; mod cool;
pub mod data; pub mod data;
@ -38,6 +40,9 @@ mod tests;
pub const VERSION: &'static str = env!("CARGO_PKG_VERSION"); pub const VERSION: &'static str = env!("CARGO_PKG_VERSION");
pub const YOPA_MAGIC : &[u8; 4] = b"YOPA";
pub const BINARY_FORMAT : u8 = 1;
/// Stupid storage with naive inefficient file persistence /// Stupid storage with naive inefficient file persistence
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct Storage { pub struct Storage {
@ -90,8 +95,14 @@ pub enum StorageError {
NotExist(Cow<'static, str>), NotExist(Cow<'static, str>),
#[error("{0}")] #[error("{0}")]
ConstraintViolation(Cow<'static, str>), ConstraintViolation(Cow<'static, str>),
#[error("{0}")]
Invalid(Cow<'static, str>),
#[error("Persistence not configured!")] #[error("Persistence not configured!")]
NotPersistent, NotPersistent,
#[error("Bad magic! Not a binary Yopa file")]
BadMagic,
#[error("Binary format {0} is not compatible with this version of Yopa")]
NotCompatible(u8),
#[error(transparent)] #[error(transparent)]
IO(#[from] std::io::Error), IO(#[from] std::io::Error),
#[error(transparent)] #[error(transparent)]
@ -164,11 +175,25 @@ impl Storage {
let f = OpenOptions::new().read(true).open(&path)?; let f = OpenOptions::new().read(true).open(&path)?;
let reader = BufReader::new(f); let mut reader = BufReader::new(f);
let parsed: Self = match self.opts.file_format { let parsed: Self = match self.opts.file_format {
FileEncoding::JSON => serde_json::from_reader(reader)?, FileEncoding::JSON => serde_json::from_reader(reader)?,
FileEncoding::BINCODE => bincode::deserialize_from(reader)?, FileEncoding::BINCODE => {
let mut magic : [u8; 5] = [0; 5];
reader.read_exact(&mut magic)?;
if &magic[0..4] != YOPA_MAGIC {
return Err(StorageError::BadMagic);
}
let version = magic[4];
if version != BINARY_FORMAT {
return Err(StorageError::NotCompatible(version));
}
bincode::deserialize_from(reader)?
},
}; };
let opts = std::mem::replace(&mut self.opts, StoreOpts::default()); let opts = std::mem::replace(&mut self.opts, StoreOpts::default());
@ -196,13 +221,17 @@ impl Storage {
.create(true) .create(true)
.truncate(true) .truncate(true)
.open(&path)?; .open(&path)?;
let writer = BufWriter::new(f); let mut writer = BufWriter::new(f);
match self.opts.file_format { match self.opts.file_format {
FileEncoding::JSON => { FileEncoding::JSON => {
serde_json::to_writer(writer, self)?; serde_json::to_writer(writer, self)?;
} }
FileEncoding::BINCODE => bincode::serialize_into(writer, self)?, FileEncoding::BINCODE => {
writer.write_all(YOPA_MAGIC)?;
writer.write_all(&[BINARY_FORMAT])?;
bincode::serialize_into(writer, self)?
},
}; };
} }
} }
@ -223,6 +252,25 @@ impl Storage {
} }
} }
/// Get object name, or the ID as string if no name is configured
pub fn get_object_name_by_id<'a>(&'a self, id : ID) -> Cow<'a, str> {
if let Some(o) = self.get_object(id) {
return self.get_object_name(o);
}
return id.to_string().into();
}
pub fn get_object_name<'b, 'a: 'b>(&'a self, object : &'a Object) -> Cow<'b, str> {
if let Some(ObjectModel{ name_property: Some(name_property), .. }) = self.get_object_model(object.model) {
if let Some(v) = self.values.values().find(|v| v.object == object.id && &v.model == name_property) {
return v.value.to_cow();
}
}
return object.id.to_string().into();
}
/// Get model name. Accepts ID of object, relation or property models. /// Get model name. Accepts ID of object, relation or property models.
pub fn get_model_name(&self, id: ID) -> &str { pub fn get_model_name(&self, id: ID) -> &str {
if let Some(x) = self.obj_models.get(&id) { if let Some(x) = self.obj_models.get(&id) {
@ -243,6 +291,11 @@ impl Storage {
self.obj_models.values() self.obj_models.values()
} }
/// Iterate object models with given IDs
pub fn get_object_models_by_ids(&self, ids: Vec<ID>) -> impl Iterator<Item = &ObjectModel> {
self.obj_models.values().filter(move |m| ids.contains(&m.id))
}
/// Get an object model by ID /// Get an object model by ID
pub fn get_object_model(&self, model_id: ID) -> Option<&ObjectModel> { pub fn get_object_model(&self, model_id: ID) -> Option<&ObjectModel> {
self.obj_models.get(&model_id) self.obj_models.get(&model_id)
@ -297,6 +350,16 @@ impl Storage {
.filter(move |model| parents.contains(&model.object)) .filter(move |model| parents.contains(&model.object))
} }
/// Find properties for a parent ID
pub fn get_property_models_for_parent(
&self,
parent: ID,
) -> impl Iterator<Item = &PropertyModel> {
self.prop_models
.values()
.filter(move |model| model.object == parent)
}
/// Get all relation models, grouped by their source object model ID /// Get all relation models, grouped by their source object model ID
pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> { pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> {
self.rel_models self.rel_models
@ -485,6 +548,12 @@ impl Storage {
} }
} }
if prop.unique && prop.multiple {
return Err(StorageError::Invalid(
"Multi-value properties cannot have the \"unique\" constraint".into(),
));
}
if self if self
.prop_models .prop_models
.iter() .iter()
@ -688,6 +757,12 @@ impl Storage {
)); ));
} }
if prop.unique && prop.multiple {
return Err(StorageError::Invalid(
"Multi-value properties cannot have the \"unique\" constraint".into(),
));
}
// Object can't be changed, so we re-fill them from the existing model // Object can't be changed, so we re-fill them from the existing model
if let Some(existing) = self.prop_models.get(&prop.id) { if let Some(existing) = self.prop_models.get(&prop.id) {
prop.object = existing.object; prop.object = existing.object;
@ -745,28 +820,10 @@ impl Storage {
} }
}; };
// validate unique name
if self
.objects
.iter()
.find(|(_, o)| o.model == obj_model_id && o.name == insobj.name)
.is_some()
{
return Err(StorageError::ConstraintViolation(
format!(
"{} named \"{}\" already exists",
self.get_model_name(obj_model_id),
insobj.name
)
.into(),
));
}
let object_id = self.next_id(); let object_id = self.next_id();
let object = data::Object { let object = data::Object {
id: object_id, id: object_id,
model: obj_model_id, model: obj_model_id,
name: insobj.name,
}; };
let find_values_to_insert = |values: Vec<InsertValue>, let find_values_to_insert = |values: Vec<InsertValue>,
@ -776,12 +833,12 @@ impl Storage {
let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model); let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model);
let mut values_to_insert = vec![]; let mut values_to_insert = vec![];
for (id, prop) in self for (prop_model_id, prop) in self
.prop_models .prop_models
.iter() .iter()
.filter(|(_id, p)| p.object == parent_model_id) .filter(|(_id, p)| p.object == parent_model_id)
{ {
if let Some(values) = values_by_id.remove(id) { if let Some(values) = values_by_id.remove(prop_model_id) {
if values.len() > 1 && !prop.multiple { if values.len() > 1 && !prop.multiple {
return Err(StorageError::ConstraintViolation( return Err(StorageError::ConstraintViolation(
format!( format!(
@ -792,6 +849,35 @@ impl Storage {
.into(), .into(),
)); ));
} }
if prop.unique {
// we know the length is at least 1. Unique should not be allowed together
// with "multiple", but if it is set so, only the first value will
// be checked.
let first = &values[0];
// validate unique name
if self
.values
.iter()
.find(|(_, o)| {
&o.model == prop_model_id
&& o.object == parent_id
&& o.value == first.value
})
.is_some()
{
return Err(StorageError::ConstraintViolation(
format!(
"Value {} already used for unique property \"{}\"",
first.value,
self.get_model_name(*prop_model_id),
)
.into(),
));
}
}
for val_instance in values { for val_instance in values {
values_to_insert.push(data::Value { values_to_insert.push(data::Value {
id: self.next_id(), id: self.next_id(),
@ -920,27 +1006,6 @@ impl Storage {
} }
}; };
// validate unique name
if self
.objects
.iter()
.find(|(_, o)| {
o.model == updated_object_model_id
&& o.name == updobj.name
&& o.id != updated_object_id
})
.is_some()
{
return Err(StorageError::ConstraintViolation(
format!(
"{} named \"{}\" already exists",
self.get_model_name(updated_object_model_id),
updobj.name
)
.into(),
));
}
// Update the object after everything else is checked // Update the object after everything else is checked
let find_values_to_change = |values: Vec<UpsertValue>, let find_values_to_change = |values: Vec<UpsertValue>,
@ -982,6 +1047,35 @@ impl Storage {
)); ));
} }
if prop.unique {
// we know the length is at least 1. Unique should not be allowed together
// with "multiple", but if it is set so, only the first value will
// be checked.
let first = &values[0];
// validate unique name
if self
.values
.iter()
.find(|(_, o)| {
&o.model == prop_model_id
&& o.object == parent_id
&& o.value == first.value
&& (first.id.is_none_or_else(|id| &o.id != id))
})
.is_some()
{
return Err(StorageError::ConstraintViolation(
format!(
"Value {} already used for unique property \"{}\"",
first.value,
self.get_model_name(*prop_model_id),
)
.into(),
));
}
}
let updated_ids = values.iter().filter_map(|v| v.id).collect_vec(); let updated_ids = values.iter().filter_map(|v| v.id).collect_vec();
ids_to_delete.extend( ids_to_delete.extend(
@ -1103,7 +1197,6 @@ impl Storage {
} }
let obj_mut = self.objects.get_mut(&updated_object_id).unwrap(); let obj_mut = self.objects.get_mut(&updated_object_id).unwrap();
obj_mut.name = updobj.name;
debug!("Add {} new object relations", relations_to_insert.len()); debug!("Add {} new object relations", relations_to_insert.len());
for rel in relations_to_insert { for rel in relations_to_insert {
@ -1130,8 +1223,9 @@ impl Storage {
/// Delete an object and associated data /// Delete an object and associated data
pub fn delete_object(&mut self, id: ID) -> Result<data::Object, StorageError> { pub fn delete_object(&mut self, id: ID) -> Result<data::Object, StorageError> {
let name = self.get_object_name_by_id(id).to_string();
return if let Some(t) = self.objects.remove(&id) { return if let Some(t) = self.objects.remove(&id) {
debug!("Delete object \"{}\"", t.name); debug!("Delete object \"{}\"", name);
// Remove relation templates // Remove relation templates
let removed_relation_ids = map_drain_filter(&mut self.relations, |_k, v| { let removed_relation_ids = map_drain_filter(&mut self.relations, |_k, v| {
v.object == id || v.related == id v.object == id || v.related == id

@ -23,6 +23,9 @@ pub struct ObjectModel {
pub id: ID, pub id: ID,
/// Template name, unique within the database /// Template name, unique within the database
pub name: String, pub name: String,
/// Property to use as the name in relation selectors
#[serde(default)]
pub name_property: Option<ID>,
} }
/// Relation between templates /// Relation between templates
@ -59,6 +62,8 @@ pub struct PropertyModel {
pub optional: bool, pub optional: bool,
/// Property can be multiple /// Property can be multiple
pub multiple: bool, pub multiple: bool,
/// Value must be unique among instances of the parent model
pub unique: bool,
/// Property data type /// Property data type
pub data_type: DataType, pub data_type: DataType,
/// Default value, used for newly created objects /// Default value, used for newly created objects

@ -22,16 +22,20 @@ pub struct UpsertRelation {
pub model: ID, pub model: ID,
/// Related model ID /// Related model ID
pub related: ID, pub related: ID,
/// Relation values /// Relation values to update or create
pub values: Vec<UpsertValue>, pub values: Vec<UpsertValue>,
} }
/// Update an existing object /// Update an existing object
#[derive(Deserialize, Debug)] #[derive(Deserialize, Debug)]
pub struct UpdateObj { pub struct UpdateObj {
/// Updated object's ID
pub id: ID, pub id: ID,
pub name: String, /// Property to use as the object's name
pub name_property: Option<ID>,
/// Values to update or create
pub values: Vec<UpsertValue>, pub values: Vec<UpsertValue>,
/// Relations to update or create
pub relations: Vec<UpsertRelation>, pub relations: Vec<UpsertRelation>,
} }

Loading…
Cancel
Save