edit form save almost working

master
Ondřej Hruška 4 years ago
parent 6561da7e99
commit bc8b04f331
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 18
      yopa-web/resources/src/components/EditObjectForm.vue
  2. 14
      yopa-web/resources/src/components/EditPropertyField.vue
  3. 12
      yopa-web/resources/src/components/EditRelationForm.vue
  4. 11
      yopa-web/resources/src/components/NewObjectForm.vue
  5. 14
      yopa-web/resources/src/components/NewRelationForm.vue
  6. 12
      yopa-web/resources/src/components/PropertyField.vue
  7. 2
      yopa-web/resources/static/bundle.js
  8. 2
      yopa-web/resources/static/bundle.js.map
  9. 36
      yopa-web/resources/templates/objects/object_detail.html.tera
  10. 15
      yopa-web/src/main.rs
  11. 2
      yopa-web/src/routes/models/object.rs
  12. 2
      yopa-web/src/routes/models/property.rs
  13. 2
      yopa-web/src/routes/models/relation.rs
  14. 114
      yopa-web/src/routes/objects.rs
  15. 16
      yopa/src/insert.rs
  16. 182
      yopa/src/lib.rs
  17. 96
      yopa/src/update.rs

@ -1,6 +1,7 @@
<script> <script>
import {castId, keyBy, objCopy, isEmpty} from "../utils"; import {castId, keyBy, objCopy, isEmpty} from "../utils";
import forEach from "lodash-es/forEach"; import forEach from "lodash-es/forEach";
import isEqual from "lodash-es/isEqual";
import axios from "axios"; import axios from "axios";
export default { export default {
@ -29,7 +30,6 @@ export default {
values[p.id] = [ values[p.id] = [
// this is the format used for values // this is the format used for values
{ {
id: null,
// it can also have model: ... here // it can also have model: ... here
value: objCopy(p.default) value: objCopy(p.default)
} }
@ -67,7 +67,11 @@ export default {
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) {
v.model_id = castId(prop_model_id); if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) {
continue;
}
v.model = castId(prop_model_id);
values.push(v); values.push(v);
} }
}) })
@ -80,7 +84,7 @@ export default {
} }
return { return {
model_id: this.object.model, // string is fine model: this.object.model, // string is fine
id: this.object.id, id: this.object.id,
name: this.name, name: this.name,
values, values,
@ -100,13 +104,13 @@ export default {
axios({ axios({
method: 'post', method: 'post',
url: '/object/update', url: `/object/update/${this.object.id}`,
data: data data: data
}) })
.then(function (response) { .then((response) => {
location.href = '/object/detail/'+this.object.id; location.href = `/object/detail/${this.object.id}`;
}) })
.catch(function (error) { .catch((error) => {
// TODO show error toast instead // TODO show error toast instead
alert(error.response ? alert(error.response ?
error.response.data : error.response.data :

@ -7,14 +7,14 @@ export default {
props: ['model', 'values'], props: ['model', 'values'],
data() { data() {
return { return {
id: uniqueId(), widget_id: uniqueId(),
fieldRefs: [], fieldRefs: [],
} }
}, },
methods: { methods: {
addValue(event) { addValue(event) {
this.values.push({ this.values.push({
id: null, model: this.model.id,
value: objCopy(this.model.default) value: objCopy(this.model.default)
}); });
@ -45,12 +45,12 @@ export default {
</td> </td>
</tr> </tr>
<tr v-else v-for="(instance, vi) in values" :key="vi"> <tr v-else v-for="(instance, vi) in values" :key="vi">
<th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="id">{{model.name}}</label></th> <th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="widget_id">{{model.name}}</label></th>
<td> <td>
<string-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='String'"></string-value> <string-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='String'"></string-value>
<integer-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Integer'"></integer-value> <integer-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Integer'"></integer-value>
<decimal-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Decimal'"></decimal-value> <decimal-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Decimal'"></decimal-value>
<boolean-value :ref="setFieldRef" :value="instance.value" :id="vi===0?id:null" v-if="model.data_type==='Boolean'"></boolean-value> <boolean-value :ref="setFieldRef" :value="instance.value" :id="vi===0?widget_id:null" v-if="model.data_type==='Boolean'"></boolean-value>
<a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a> <a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a>
</td> </td>
</tr> </tr>

@ -1,6 +1,7 @@
<script> <script>
import {castId, isEmpty, keyBy, objCopy} from "../utils"; import {castId, isEmpty, keyBy, objCopy} from "../utils";
import forEach from "lodash-es/forEach"; import forEach from "lodash-es/forEach";
import isEqual from "lodash-es/isEqual";
export default { export default {
props: ['model_id', 'schema', 'objects', 'initialInstances'], props: ['model_id', 'schema', 'objects', 'initialInstances'],
@ -14,7 +15,7 @@ export default {
properties.sort((a, b) => a.name.localeCompare(b.name)); properties.sort((a, b) => a.name.localeCompare(b.name));
if (isEmpty(properties)) { if (isEmpty(properties)) {
properties = null; properties = [];
} else { } else {
properties = keyBy(properties, 'id'); properties = keyBy(properties, 'id');
} }
@ -55,11 +56,15 @@ export default {
let values = []; let values = [];
forEach(instance.values, (vv, prop_model_id) => { forEach(instance.values, (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
v.model_id = castId(prop_model_id); if (isEqual(v.value, {"String": ""}) && this.properties[prop_model_id].optional) {
continue;
}
v.model = castId(prop_model_id);
values.push(v); values.push(v);
} }
}) })
instance.model_id = this.model.id; instance.related = castId(instance.related);
instance.model = this.model.id;
instance.values = values; instance.values = values;
relations.push(instance); relations.push(instance);
}) })
@ -78,7 +83,6 @@ export default {
} }
}); });
this.instances.push({ this.instances.push({
id: null,
related: '', related: '',
values values
}) })

@ -2,6 +2,7 @@
import {castId, keyBy, objCopy, isEmpty} from "../utils"; import {castId, keyBy, objCopy, isEmpty} from "../utils";
import forEach from "lodash-es/forEach"; import forEach from "lodash-es/forEach";
import axios from "axios"; import axios from "axios";
import isEqual from "lodash-es/isEqual";
export default { export default {
props: ['model_id', 'schema', 'objects'], props: ['model_id', 'schema', 'objects'],
@ -50,10 +51,14 @@ export default {
} }
let values = []; let values = [];
forEach(objCopy(this.values), (vv, k) => { forEach(objCopy(this.values), (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
if (isEqual(v, {"String": ""}) && this.properties[prop_model_id].optional) {
continue;
}
values.push({ values.push({
model_id: castId(k), model: castId(prop_model_id),
value: v value: v
}) })
} }
@ -67,7 +72,7 @@ export default {
} }
return { return {
model_id: this.model_id, // string is fine model: this.model.id,
name: this.name, name: this.name,
values, values,
relations, relations,

@ -1,6 +1,7 @@
<script> <script>
import {castId, keyBy, objCopy, isEmpty} from "../utils"; import {castId, keyBy, objCopy, isEmpty} from "../utils";
import forEach from "lodash-es/forEach"; import forEach from "lodash-es/forEach";
import isEqual from "lodash-es/isEqual";
export default { export default {
props: ['model_id', 'schema', 'objects'], props: ['model_id', 'schema', 'objects'],
@ -14,7 +15,7 @@ export default {
properties.sort((a, b) => a.name.localeCompare(b.name)); properties.sort((a, b) => a.name.localeCompare(b.name));
if (isEmpty(properties)) { if (isEmpty(properties)) {
properties = null; properties = [];
} else { } else {
properties = keyBy(properties, 'id'); properties = keyBy(properties, 'id');
} }
@ -70,16 +71,20 @@ export default {
forEach(instance.values, (vv, prop_model_id) => { forEach(instance.values, (vv, prop_model_id) => {
for (let v of vv) { for (let v of vv) {
if (isEqual(v, {"String": ""}) && this.properties[prop_model_id].optional) {
continue;
}
values.push({ values.push({
model_id: castId(prop_model_id), model: castId(prop_model_id),
value: v value: v
}); });
} }
}) })
relations.push({ relations.push({
model_id: this.model.id, model: this.model.id,
related_id: castId(instance.related), related: castId(instance.related),
values values
}); });
}) })
@ -96,6 +101,7 @@ export default {
} }
}); });
this.instances.push({ this.instances.push({
model: this.model.id,
related: '', related: '',
values values
}) })

@ -7,7 +7,7 @@ export default {
props: ['model', 'values'], props: ['model', 'values'],
data() { data() {
return { return {
id: uniqueId(), widget_id: uniqueId(),
fieldRefs: [], fieldRefs: [],
} }
}, },
@ -42,12 +42,12 @@ export default {
</td> </td>
</tr> </tr>
<tr v-else v-for="(value, vi) in values" :key="vi"> <tr v-else v-for="(value, vi) in values" :key="vi">
<th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="id">{{model.name}}</label></th> <th :rowspan="values.length + model.multiple" v-if="vi == 0"><label :for="widget_id">{{model.name}}</label></th>
<td> <td>
<string-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='String'"></string-value> <string-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='String'"></string-value>
<integer-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='Integer'"></integer-value> <integer-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Integer'"></integer-value>
<decimal-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='Decimal'"></decimal-value> <decimal-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Decimal'"></decimal-value>
<boolean-value :ref="setFieldRef" :value="value" :id="vi===0?id:null" v-if="model.data_type==='Boolean'"></boolean-value> <boolean-value :ref="setFieldRef" :value="value" :id="vi===0?widget_id:null" v-if="model.data_type==='Boolean'"></boolean-value>
<a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a> <a href="#" @click="removeValue(vi)" v-if="vi > 0 || model.optional" style="margin-left:5px">X</a>
</td> </td>
</tr> </tr>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -6,6 +6,7 @@
{% block nav -%} {% block nav -%}
<a href="/">Home</a> <a href="/">Home</a>
<a href="/object/update/{{object.id}}">Edit</a>
{%- endblock %} {%- endblock %}
{% block content -%} {% block content -%}
@ -35,7 +36,7 @@
<ul> <ul>
{% for instance in relation.instances %} {% for instance in relation.instances %}
<li> <li>
<b>{{instance.related.name}}</b> <b><a href="/object/detail/{{instance.related.id}}">{{instance.related.name}}</a></b>
{% if instance.properties %} {% if instance.properties %}
<br> <br>
@ -61,4 +62,37 @@
{% endfor %} {% endfor %}
{% for relation in reciprocal_relations %}
<h3>{{relation.model.reciprocal_name}}</h3>
<ul>
{% for instance in relation.instances %}
<li>
<b><a href="/object/detail/{{instance.related.id}}">{{instance.related.name}}</a></b>
{% if instance.properties %}
<br>
<small>
{% for property in instance.properties %}
{%- if 0 != loop.index0 -%}
;
{%- endif -%}
{% for value in property.values %}
{%- if loop.first -%}
<i>{{ property.model.name }}:</i>
{%- else -%}
,
{% endif -%}
{{ value.value | print_typed_value }}
{% endfor %}
{% endfor %}
</small>
{% endif %}
</li>
{% endfor %}
</ul>
{% endfor %}
{%- endblock %} {%- endblock %}

@ -159,6 +159,7 @@ async fn main() -> std::io::Result<()> {
.service(routes::objects::create) .service(routes::objects::create)
.service(routes::objects::detail) .service(routes::objects::detail)
.service(routes::objects::update_form) .service(routes::objects::update_form)
.service(routes::objects::update)
// //
.service(static_files) .service(static_files)
.default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found"))) .default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found")))
@ -241,35 +242,35 @@ fn init_yopa() -> YopaStoreWrapper {
}).unwrap(); }).unwrap();
let book1 = store.insert_object(InsertObj { let book1 = store.insert_object(InsertObj {
model_id: id_book, model: id_book,
name: "Book 1".to_string(), name: "Book 1".to_string(),
values: vec![], values: vec![],
relations: vec![] relations: vec![]
}).unwrap(); }).unwrap();
store.insert_object(InsertObj { store.insert_object(InsertObj {
model_id: id_book, model: id_book,
name: "Book 2".to_string(), name: "Book 2".to_string(),
values: vec![], values: vec![],
relations: vec![] relations: vec![]
}).unwrap(); }).unwrap();
store.insert_object(InsertObj { store.insert_object(InsertObj {
model_id: id_recipe, model: id_recipe,
name: "Recipe1".to_string(), name: "Recipe1".to_string(),
values: vec![ values: vec![
InsertValue { InsertValue {
model_id: val_descr, model: val_descr,
value: TypedValue::String("Bla bla bla".into()) value: TypedValue::String("Bla bla bla".into())
} }
], ],
relations: vec![ relations: vec![
InsertRel { InsertRel {
model_id: rel_book_id, model: rel_book_id,
related_id: book1, related: book1,
values: vec![ values: vec![
InsertValue { InsertValue {
model_id: page, model: page,
value: TypedValue::Integer(15) value: TypedValue::Integer(15)
} }
] ]

@ -102,7 +102,7 @@ pub(crate) async fn update(
let form = form.into_inner(); let form = form.into_inner();
let id = model_id.into_inner(); let id = model_id.into_inner();
match wg.update_object(ObjectModel { match wg.update_object_model(ObjectModel {
id, id,
name: form.name.clone(), name: form.name.clone(),
}) { }) {

@ -244,7 +244,7 @@ pub(crate) async fn update(
} }
}; };
match wg.update_property(PropertyModel { match wg.update_property_model(PropertyModel {
id, id,
object: Default::default(), // dummy object: Default::default(), // dummy
name: form.name.clone(), name: form.name.clone(),

@ -174,7 +174,7 @@ pub(crate) async fn update(
let form = form.into_inner(); let form = form.into_inner();
let id = model_id.into_inner(); let id = model_id.into_inner();
match wg.update_relation(RelationModel { match wg.update_relation_model(RelationModel {
id, id,
object: Default::default(), // dummy object: Default::default(), // dummy
name: form.name.clone(), name: form.name.clone(),

@ -4,10 +4,10 @@ use crate::session_ext::SessionExt;
use crate::routes::models::object::ObjectModelForm; use crate::routes::models::object::ObjectModelForm;
use crate::TERA; use crate::TERA;
use crate::tera_ext::TeraExt; use crate::tera_ext::TeraExt;
use yopa::{ID, model, Storage, data}; use yopa::{ID, model, Storage, data, TypedValue};
use yopa::data::Object; use yopa::data::Object;
use serde::{Serialize,Deserialize}; use serde::{Serialize,Deserialize};
use yopa::insert::InsertObj; use yopa::insert::{InsertObj, InsertValue};
use crate::utils::redirect; use crate::utils::redirect;
use actix_web::web::Json; use actix_web::web::Json;
use serde_json::Value; use serde_json::Value;
@ -16,6 +16,7 @@ use yopa::model::{ObjectModel, PropertyModel, RelationModel};
use heck::TitleCase; use heck::TitleCase;
use std::collections::HashMap; use std::collections::HashMap;
use json_dotpath::DotPaths; use json_dotpath::DotPaths;
use yopa::update::UpdateObj;
// 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
@ -102,7 +103,7 @@ pub(crate) async fn create(
let form = form.into_inner(); let form = form.into_inner();
let name = form.name.clone(); let name = form.name.clone();
let model_name = wg.get_model_name(form.model_id).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) => {
@ -174,11 +175,13 @@ struct RelationInstanceView<'a> {
#[get("/object/detail/{id}")] #[get("/object/detail/{id}")]
pub(crate) async fn detail( pub(crate) async fn detail(
id: web::Path<ID>, id: web::Path<ID>,
store: crate::YopaStoreWrapper store: crate::YopaStoreWrapper,
session: Session
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let object_id = *id; let object_id = *id;
let mut context = tera::Context::new(); let mut context = tera::Context::new();
session.render_flash(&mut context);
let rg = store.read().await; let rg = store.read().await;
@ -192,14 +195,15 @@ pub(crate) async fn detail(
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));
let relations = rg.get_relations_for_object(object_id).collect_vec(); let mut relations = rg.get_relations_for_object(object_id).collect_vec();
let reci_relations = rg.get_reciprocal_relations_for_object(object_id).collect_vec();
// values by parent ID // values by parent ID
let mut ids_to_get_values_for = relations.iter().map(|r| r.id).collect_vec(); let mut ids_to_get_values_for = relations.iter().map(|r| r.id).collect_vec();
ids_to_get_values_for.extend(reci_relations.iter().map(|r| r.id));
ids_to_get_values_for.push(object_id); ids_to_get_values_for.push(object_id);
let mut grouped_values = {
rg.get_grouped_values_for_objects(ids_to_get_values_for) let mut grouped_values = rg.get_grouped_values_for_objects(ids_to_get_values_for);
};
// object's own properties // object's own properties
{ {
@ -214,6 +218,9 @@ pub(crate) async fn detail(
values values
}) })
} }
view_object_properties.sort_by_key(|p| &p.model.name);
context.insert("properties", &view_object_properties); context.insert("properties", &view_object_properties);
} }
@ -244,21 +251,77 @@ pub(crate) async fn detail(
}) })
} }
view_rel_properties.sort_by_key(|p| &p.model.name);
instances.push(RelationInstanceView { instances.push(RelationInstanceView {
related: related_obj, related: related_obj,
properties: view_rel_properties properties: view_rel_properties
}) })
} }
instances.sort_by_key(|r| &r.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(),
related_name: rg.get_model_name(model_id), related_name: rg.get_model_name(model_id),
instances instances
}) })
} }
relation_views.sort_by_key(|r| &r.model.name);
context.insert("relations", &relation_views); context.insert("relations", &relation_views);
} }
// near-copypasta for reciprocal
{
let grouped_relations = reci_relations.iter()
.into_group_map_by(|relation| relation.model);
let mut relation_views = vec![];
for (model_id, relations) in grouped_relations {
let mut instances = vec![];
for rel in relations {
let related_obj = match rg.get_object(rel.object) {
None => continue,
Some(obj) => obj
};
let mut rel_values_by_model = grouped_values
.remove(&rel.id).unwrap_or_default().into_iter()
.into_group_map_by(|value| value.model);
let mut view_rel_properties = vec![];
for (prop_model_id, values) in rel_values_by_model {
view_rel_properties.push(PropertyView {
model: rg.get_property_model(prop_model_id).unwrap(),
values
})
}
view_rel_properties.sort_by_key(|p| &p.model.name);
instances.push(RelationInstanceView {
related: related_obj,
properties: view_rel_properties
})
}
instances.sort_by_key(|r| &r.related.name);
relation_views.push(RelationView {
model: rg.get_relation_model(model_id).unwrap(),
related_name: rg.get_model_name(model_id),
instances
})
}
relation_views.sort_by_key(|r| &r.model.reciprocal_name);
context.insert("reciprocal_relations", &relation_views);
}
TERA.build_response("objects/object_detail", &context) TERA.build_response("objects/object_detail", &context)
} }
@ -280,9 +343,6 @@ struct EnrichedRelation<'a> {
values: HashMap<String /* ID */, Vec<&'a data::Value>>, values: HashMap<String /* ID */, Vec<&'a data::Value>>,
} }
// FIXME relation values are not showing in the edit form!
// TODO save handling
#[get("/object/update/{id}")] #[get("/object/update/{id}")]
pub(crate) async fn update_form( pub(crate) async fn update_form(
id: web::Path<ID>, id: web::Path<ID>,
@ -382,3 +442,35 @@ pub(crate) async fn update_form(
TERA.build_response("objects/object_update", &context) TERA.build_response("objects/object_update", &context)
} }
#[post("/object/update/{id}")]
pub(crate) async fn update(
id: web::Path<ID>,
form: web::Json<UpdateObj>,
store: crate::YopaStoreWrapper,
session: Session,
) -> actix_web::Result<HttpResponse> {
warn!("{:?}", form);
let mut wg = store.write().await;
let form = form.into_inner();
let name = form.name.clone();
let object = wg.get_object(form.id)
.ok_or_else(|| actix_web::error::ErrorNotFound("No such object"))?;
let model_name = wg.get_model_name(object.model).to_owned().to_title_case();
match wg.update_object(form) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("{} \"{}\" updated.", model_name, name));
Ok(HttpResponse::Ok().finish())
}
Err(e) => {
warn!("Error updating model: {}", e);
Ok(HttpResponse::BadRequest().body(e.to_string()))
}
}
}

@ -8,14 +8,14 @@ use super::ID;
/// Value to insert /// Value to insert
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertValue { pub struct InsertValue {
pub model_id: ID, pub model: ID,
pub value: TypedValue, pub value: TypedValue,
} }
impl InsertValue { impl InsertValue {
pub fn new(model_id: ID, value: TypedValue) -> Self { pub fn new(model_id: ID, value: TypedValue) -> Self {
Self { Self {
model_id, model: model_id,
value, value,
} }
} }
@ -24,16 +24,16 @@ impl InsertValue {
/// Info for inserting a relation /// Info for inserting a relation
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertRel { pub struct InsertRel {
pub model_id: ID, pub model: ID,
pub related_id: ID, pub related: ID,
pub values: Vec<InsertValue>, pub values: Vec<InsertValue>,
} }
impl InsertRel { impl InsertRel {
pub fn new(model_id: ID, related_id: ID, values: Vec<InsertValue>) -> Self { pub fn new(model_id: ID, related_id: ID, values: Vec<InsertValue>) -> Self {
Self { Self {
model_id, model: model_id,
related_id, related: related_id,
values, values,
} }
} }
@ -42,7 +42,7 @@ impl InsertRel {
/// Info for inserting a relation /// Info for inserting a relation
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InsertObj { pub struct InsertObj {
pub model_id: ID, pub model: ID,
pub name : String, pub name : String,
pub values: Vec<InsertValue>, pub values: Vec<InsertValue>,
pub relations: Vec<InsertRel>, pub relations: Vec<InsertRel>,
@ -51,7 +51,7 @@ pub struct InsertObj {
impl InsertObj { impl InsertObj {
pub fn new(model_id: ID, name : String, values: Vec<InsertValue>, relations: Vec<InsertRel>) -> Self { pub fn new(model_id: ID, name : String, values: Vec<InsertValue>, relations: Vec<InsertRel>) -> Self {
Self { Self {
model_id, model: model_id,
name, name,
values, values,
relations, relations,

@ -2,7 +2,7 @@
#[macro_use] extern crate log; #[macro_use] extern crate log;
use std::borrow::Cow; use std::borrow::Cow;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use itertools::Itertools; use itertools::Itertools;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -18,11 +18,13 @@ use crate::model::{PropertyModel, RelationModel};
pub use data::{TypedValue}; pub use data::{TypedValue};
pub use model::{DataType}; pub use model::{DataType};
use crate::data::Object; use crate::data::{Object, Value};
use crate::update::{UpdateObj, UpsertValue};
pub mod model; pub mod model;
pub mod data; pub mod data;
pub mod insert; pub mod insert;
pub mod update;
pub mod id; pub mod id;
mod cool; mod cool;
@ -45,7 +47,7 @@ pub struct Storage {
#[serde(with = "serde_map_as_list")] #[serde(with = "serde_map_as_list")]
relations: HashMap<ID, data::Relation>, relations: HashMap<ID, data::Relation>,
#[serde(with = "serde_map_as_list")] #[serde(with = "serde_map_as_list")]
properties: HashMap<ID, data::Value>, values: HashMap<ID, data::Value>,
} }
@ -162,7 +164,7 @@ impl Storage {
let _ = map_drain_filter(&mut self.objects, |_k, v| v.model == id); let _ = map_drain_filter(&mut self.objects, |_k, v| v.model == id);
// Remove property values // Remove property values
let _ = map_drain_filter(&mut self.properties, |_k, v| removed_prop_ids.contains(&v.model)); let _ = map_drain_filter(&mut self.values, |_k, v| removed_prop_ids.contains(&v.model));
// Remove relations // Remove relations
let _ = map_drain_filter(&mut self.relations, |_k, v| removed_relation_ids.contains(&v.model)); let _ = map_drain_filter(&mut self.relations, |_k, v| removed_relation_ids.contains(&v.model));
@ -186,7 +188,7 @@ impl Storage {
// Remove related property templates // Remove related property templates
let removed_prop_tpl_ids = map_drain_filter(&mut self.prop_models, |_k, v| v.object == id).keys(); let removed_prop_tpl_ids = map_drain_filter(&mut self.prop_models, |_k, v| v.object == id).keys();
let _ = map_drain_filter(&mut self.properties, |_k, v| removed_prop_tpl_ids.contains(&v.model)); let _ = map_drain_filter(&mut self.values, |_k, v| removed_prop_tpl_ids.contains(&v.model));
// Related object remain untouched, so there can be a problem with orphans. This is up to the application to deal with. // Related object remain untouched, so there can be a problem with orphans. This is up to the application to deal with.
@ -202,7 +204,7 @@ impl Storage {
debug!("Undefine property model \"{}\"", t.name); debug!("Undefine property model \"{}\"", t.name);
// Remove relations // Remove relations
let _ = map_drain_filter(&mut self.properties, |_k, v| v.model == id); let _ = map_drain_filter(&mut self.values, |_k, v| v.model == id);
Ok(t) Ok(t)
} else { } else {
Err(StorageError::NotExist(format!("property model {}", id).into())) Err(StorageError::NotExist(format!("property model {}", id).into()))
@ -237,7 +239,7 @@ impl Storage {
/// Insert object with relations, validating the data model constraints /// Insert object with relations, validating the data model constraints
pub fn insert_object(&mut self, insobj: InsertObj) -> Result<ID, StorageError> { pub fn insert_object(&mut self, insobj: InsertObj) -> Result<ID, StorageError> {
let obj_model_id = insobj.model_id; let obj_model_id = insobj.model;
debug!("Insert object {:?}", insobj); debug!("Insert object {:?}", insobj);
let obj_model = match self.obj_models.get(&obj_model_id) { let obj_model = match self.obj_models.get(&obj_model_id) {
@ -259,7 +261,7 @@ impl Storage {
}; };
let find_values_to_insert = |values: Vec<InsertValue>, parent_id : ID, parent_model_id: ID| -> Result<Vec<data::Value>, StorageError> { let find_values_to_insert = |values: Vec<InsertValue>, parent_id : ID, parent_model_id: ID| -> Result<Vec<data::Value>, StorageError> {
let mut values_by_id = values.into_iter().into_group_map_by(|iv| iv.model_id); 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.prop_models.iter().filter(|(_id, p)| p.object == parent_model_id) { for (id, prop) in self.prop_models.iter().filter(|(_id, p)| p.object == parent_model_id) {
@ -294,7 +296,7 @@ impl Storage {
let mut values_to_insert = find_values_to_insert(insobj.values, object_id, obj_model_id)?; let mut values_to_insert = find_values_to_insert(insobj.values, object_id, obj_model_id)?;
// And now ..... relations! // And now ..... relations!
let mut relations_by_id = insobj.relations.into_iter().into_group_map_by(|ir| ir.model_id); let mut relations_by_id = insobj.relations.into_iter().into_group_map_by(|ir| ir.model);
let mut relations_to_insert = vec![]; let mut relations_to_insert = vec![];
for (relation_model_id, relation_model) in self.rel_models.iter().filter(|(_id, r)| r.object == obj_model_id) { for (relation_model_id, relation_model) in self.rel_models.iter().filter(|(_id, r)| r.object == obj_model_id) {
@ -304,7 +306,7 @@ impl Storage {
} }
for rel_instance in instances { for rel_instance in instances {
if let Some(related) = self.objects.get(&rel_instance.related_id) { if let Some(related) = self.objects.get(&rel_instance.related) {
if related.model != relation_model.related { if related.model != relation_model.related {
return Err(StorageError::ConstraintViolation( return Err(StorageError::ConstraintViolation(
format!("{} of {} requires object of type {}, got {}", format!("{} of {} requires object of type {}, got {}",
@ -322,8 +324,8 @@ impl Storage {
relations_to_insert.push(data::Relation { relations_to_insert.push(data::Relation {
id: relation_id, id: relation_id,
object: object_id, object: object_id,
model: rel_instance.model_id, model: rel_instance.model,
related: rel_instance.related_id, related: rel_instance.related,
}); });
} }
} else { } else {
@ -340,7 +342,7 @@ impl Storage {
} }
for value in values_to_insert { for value in values_to_insert {
self.properties.insert(value.id, value); self.values.insert(value.id, value);
} }
Ok(object_id) Ok(object_id)
@ -379,13 +381,18 @@ impl Storage {
.filter(move |rel| rel.object == object_id) .filter(move |rel| rel.object == object_id)
} }
pub fn get_reciprocal_relations_for_object(&self, object_id: ID) -> impl Iterator<Item=&data::Relation> {
self.relations.values()
.filter(move |rel| rel.related == object_id)
}
pub fn get_values_for_object(&self, object_id: ID) -> impl Iterator<Item=&data::Value> { pub fn get_values_for_object(&self, object_id: ID) -> impl Iterator<Item=&data::Value> {
self.properties.values() self.values.values()
.filter(move |prop| prop.object == object_id) .filter(move |prop| prop.object == object_id)
} }
pub fn get_grouped_values_for_objects(&self, parents: Vec<ID>) -> HashMap<ID, Vec<&data::Value>> { pub fn get_grouped_values_for_objects(&self, parents: Vec<ID>) -> HashMap<ID, Vec<&data::Value>> {
self.properties.values() self.values.values()
.filter(move |prop| parents.contains(&prop.object)) .filter(move |prop| parents.contains(&prop.object))
.into_group_map_by(|model| model.object) .into_group_map_by(|model| model.object)
} }
@ -425,7 +432,146 @@ impl Storage {
} }
// Updates // Updates
pub fn update_object(&mut self, model : ObjectModel) -> Result<(), StorageError> { pub fn update_object(&mut self, updobj: UpdateObj) -> Result<(), StorageError> {
let old_object = self.objects.get(&updobj.id)
.ok_or_else(|| StorageError::ConstraintViolation(format!("Object does not exist").into()))?;
let updated_object_id = old_object.id;
let updated_object_model_id = old_object.model;
debug!("Update object {:?}", updobj);
let obj_model = match self.obj_models.get(&updated_object_model_id) {
Some(m) => m,
None => return Err(StorageError::NotExist(format!("object model {}", updated_object_model_id).into()))
};
// 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
let find_values_to_change = |values: Vec<UpsertValue>, parent_id : ID, parent_model_id: ID| -> Result<(
// Insert (can overwrite existing, the ID will not change)
Vec<data::Value>,
// Delete
Vec<ID>
), StorageError> {
let mut values_by_model = values.into_iter().into_group_map_by(|iv| iv.model);
let mut values_to_insert = vec![];
let mut ids_to_delete = vec![];
let mut existing_values_by_id = self.values.values()
.filter(|v| v.object == parent_id)
.into_group_map_by(|v| v.model);
for (prop_model_id, prop) in self.prop_models.iter().filter(|(_id, p)| p.object == parent_model_id) {
if let Some(values) = values_by_model.remove(prop_model_id) {
if values.len() > 1 && !prop.multiple {
return Err(StorageError::ConstraintViolation(format!("{} of {} cannot have multiple values", prop, self.describe_model(parent_model_id)).into()));
}
let updated_ids = values.iter().filter_map(|v| v.id).collect_vec();
ids_to_delete.extend(existing_values_by_id.remove(&prop.id).unwrap_or_default().into_iter()
.filter(|v| !updated_ids.contains(&v.id)).map(|v| v.id));
for val_instance in values {
values_to_insert.push(data::Value {
id: val_instance.id.unwrap_or_else(|| next_id()),
object: parent_id,
model: prop.id,
value: val_instance.value
});
}
} else {
if !prop.optional {
warn!("Attempt to remove non-optional prop, do nothing");
} else {
if let Some(existing) = existing_values_by_id.remove(&prop.id) {
ids_to_delete.extend(existing.into_iter().map(|v| v.id));
}
}
}
}
Ok((values_to_insert, ids_to_delete))
};
let (mut values_to_insert, mut value_ids_to_delete) = find_values_to_change(updobj.values, updated_object_id, updated_object_model_id)?;
// And now ..... relations!
let mut relations_by_model = updobj.relations.into_iter().into_group_map_by(|ir| ir.model);
let mut relations_to_insert = vec![];
let mut relations_to_delete = vec![];
let existing_relations_by_id = self.relations.values()
.filter(|v| v.object == updated_object_id)
.into_group_map_by(|v| v.model);
for (relation_model_id, relation_model) in self.rel_models.iter().filter(|(_id, r)| r.object == updated_object_model_id) {
if let Some(instances) = relations_by_model.remove(relation_model_id) {
if instances.len() > 1 && !relation_model.multiple {
return Err(StorageError::ConstraintViolation(format!("{} of {} cannot be set multiply", relation_model, obj_model).into()));
}
for rel_instance in instances {
if let Some(related) = self.objects.get(&rel_instance.related) {
if related.model != relation_model.related {
return Err(StorageError::ConstraintViolation(
format!("{} of {} requires object of type {}, got {}",
relation_model, obj_model,
self.describe_model(relation_model.related),
self.describe_model(related.model)).into()));
}
}
let relation_id = rel_instance.id.unwrap_or_else(|| next_id());
// Relations can have properties
let (ins, del) = find_values_to_change(rel_instance.values, relation_id, *relation_model_id)?;
values_to_insert.extend(ins);
value_ids_to_delete.extend(del);
relations_to_insert.push(data::Relation {
id: relation_id,
object: updated_object_id,
model: rel_instance.model,
related: rel_instance.related,
});
}
} else {
if !relation_model.optional {
return Err(StorageError::ConstraintViolation(format!("{} is required for {}", relation_model, obj_model).into()));
}
}
}
let obj_mut = self.objects.get_mut(&updated_object_id).unwrap();
obj_mut.name = updobj.name;
for rel in relations_to_insert {
self.relations.insert(rel.id, rel);
}
for value in values_to_insert {
self.values.insert(value.id, value);
}
for id in value_ids_to_delete {
self.values.remove(&id);
}
for id in relations_to_delete {
self.relations.remove(&id);
}
Ok(())
}
pub fn update_object_model(&mut self, model : ObjectModel) -> Result<(), StorageError> {
if model.name.is_empty() { if model.name.is_empty() {
return Err(StorageError::ConstraintViolation(format!("Model name must not be empty.").into())); return Err(StorageError::ConstraintViolation(format!("Model name must not be empty.").into()));
} }
@ -442,7 +588,7 @@ impl Storage {
Ok(()) Ok(())
} }
pub fn update_relation(&mut self, mut rel : RelationModel) -> Result<(), StorageError> { pub fn update_relation_model(&mut self, mut rel : RelationModel) -> Result<(), StorageError> {
if rel.name.is_empty() || rel.reciprocal_name.is_empty() { if rel.name.is_empty() || rel.reciprocal_name.is_empty() {
return Err(StorageError::ConstraintViolation(format!("Relation names must not be empty.").into())); return Err(StorageError::ConstraintViolation(format!("Relation names must not be empty.").into()));
} }
@ -475,7 +621,7 @@ impl Storage {
Ok(()) Ok(())
} }
pub fn update_property(&mut self, mut prop: PropertyModel) -> Result<(), StorageError> { pub fn update_property_model(&mut self, mut prop: PropertyModel) -> Result<(), StorageError> {
if prop.name.is_empty() { if prop.name.is_empty() {
return Err(StorageError::ConstraintViolation(format!("Property name must not be empty.").into())); return Err(StorageError::ConstraintViolation(format!("Property name must not be empty.").into()));
} }

@ -0,0 +1,96 @@
use serde::{Deserialize, Serialize};
use crate::{ID, TypedValue};
/// Update or insert a value
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpsertValue {
/// PK, null if creating
pub id: Option<ID>,
/// Property template ID
pub model: ID,
/// Property value
pub value: TypedValue,
}
/// Update or insert a relation
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UpsertRelation {
/// PK, null if creating
pub id: Option<ID>,
/// Relation template ID
pub model: ID,
/// Related model ID
pub related: ID,
/// Relation values
pub values: Vec<UpsertValue>,
}
/// Update an existing object
#[derive(Deserialize,Debug)]
pub struct UpdateObj {
pub id: ID,
pub name: String,
pub values: Vec<UpsertValue>,
pub relations: Vec<UpsertRelation>
}
#[cfg(test)]
mod tests {
use crate::update::UpdateObj;
#[test]
#[cfg(not(feature = "uuid-ids"))]
fn deserialize() {
let json = json!({
"model_id": 0,
"id": 10,
"name": "Recipe1",
"values": [
{
"id": 11,
"model": 3,
"object": 10,
"value": {
"String": "Bla bla bla"
}
},
{
"value": {
"String": "sdfsdfsdf"
},
"model": 3
}
],
"relations": [
{
"id": 12,
"model": 5,
"object": 10,
"related": 8,
"values": [
{
"id": 13,
"model": 6,
"object": 12,
"value": {
"Integer": 15
},
"model": 6
}
]
}
]
});
let obj : UpdateObj = serde_json::from_value(json).unwrap();
}
#[test]
#[cfg(not(feature = "uuid-ids"))]
fn deserialize2() {
let json = json!();
let obj : UpdateObj = serde_json::from_value(json).unwrap();
}
}
Loading…
Cancel
Save