list of objects and some other stuff

master
Ondřej Hruška 3 years ago
parent b5a4900209
commit 97af2fd924
Signed by: MightyPork
GPG Key ID: 2C5FD5035250423D
  1. 2
      Cargo.lock
  2. 3
      yopa-web/Cargo.toml
  3. 2
      yopa-web/Makefile
  4. 27
      yopa-web/resources/src/components/NewObjectForm.vue
  5. 6
      yopa-web/resources/src/main.js
  6. 2
      yopa-web/resources/static/bundle.js
  7. 2
      yopa-web/resources/static/bundle.js.map
  8. 4
      yopa-web/resources/templates/models/index.html.tera
  9. 29
      yopa-web/resources/templates/objects/index.html.tera
  10. 39
      yopa-web/src/main.rs
  11. 73
      yopa-web/src/routes.rs
  12. 73
      yopa-web/src/routes/models.rs
  13. 8
      yopa-web/src/routes/models/object.rs
  14. 10
      yopa-web/src/routes/models/property.rs
  15. 10
      yopa-web/src/routes/models/relation.rs
  16. 45
      yopa-web/src/routes/objects.rs
  17. 31
      yopa/src/lib.rs

2
Cargo.lock generated

@ -2564,7 +2564,9 @@ dependencies = [
"actix-session",
"actix-web",
"actix-web-static-files",
"heck",
"include_dir",
"itertools",
"log",
"once_cell",
"parking_lot",

@ -11,7 +11,7 @@ build = "build.rs"
yopa = { path = "../yopa", features = [] }
serde = "1"
serde_json = "1"
heck = "0.3.2"
log = "0.4.14"
simple-logging = "2.0.2"
actix-web = "3"
@ -22,6 +22,7 @@ tera = "1.6.1"
actix-web-static-files = "3.0"
once_cell = "1.5.2"
rand = "0.8.3"
itertools = "0.10.0"
tokio = { version="0.2.6", features=["full"] }

@ -0,0 +1,2 @@
assets:
cd resources && npm run build

@ -90,10 +90,13 @@ export default {
data: data
})
.then(function (response) {
console.log('Response', response);
location.href = '/objects';
})
.catch(function (error) {
console.log('Error', error);
// TODO show error toast instead
alert(error.response ?
error.response.data :
error)
});
},
@ -129,13 +132,15 @@ export default {
<property v-for="(property, pi) in properties" :model="property" :values="values[property.id]" :key="pi"></property>
</table>
<h3>Relations</h3>
<new-relation
v-for="relation in relations"
:ref="setRelationRef"
:model_id="relation.id"
:objects="objects"
:schema="schema"
></new-relation>
<div v-if="relations.length > 0">
<h3>Relations</h3>
<new-relation
v-for="relation in relations"
:ref="setRelationRef"
:model_id="relation.id"
:objects="objects"
:schema="schema"
></new-relation>
</div>
</template>

@ -34,3 +34,9 @@ window.Yopa = {
return instance;
}
};
onLoad(() => {
setTimeout(() => {
document.getElementsByClassName('toast')[0].style.display = 'none';
}, 3000)
})

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

@ -6,12 +6,12 @@
{%- endblock %}
{% block nav -%}
<a href="/takeout">Takeout</a>
<a href="/">Home</a>
{%- endblock %}
{% block content -%}
<h1>Welcome to YOPA</h1>
<h1>Models</h1>
<a href="/model/object/create">New model</a>

@ -0,0 +1,29 @@
{% extends "_layout" %}
{% block title -%}
Objects
{%- endblock %}
{% block nav -%}
<a href="/models">Edit models</a>
<a href="/takeout">JSON</a>
{%- endblock %}
{% block content -%}
<h1>Objects</h1>
{% for table in models %}
<h2>{{ table.model.name }}</h2>
<a href="/object/create/{{table.model.id}}">Add {{ table.model.name }}</a>
<ul>
{% for object in table.objects %}
<li><a href="/object/detail/{{object.id}}">{{object.name}}</a>
{% endfor %}
</ul>
{% endfor %}
{%- endblock %}

@ -133,26 +133,29 @@ async fn main() -> std::io::Result<()> {
.service(routes::index)
.service(routes::takeout)
//
.service(routes::object_model::create_form)
.service(routes::object_model::create)
.service(routes::object_model::update_form)
.service(routes::object_model::update)
.service(routes::object_model::delete)
.service(routes::models::list)
//
.service(routes::relation_model::create_form)
.service(routes::relation_model::create)
.service(routes::relation_model::update_form)
.service(routes::relation_model::update)
.service(routes::relation_model::delete)
.service(routes::models::object::create_form)
.service(routes::models::object::create)
.service(routes::models::object::update_form)
.service(routes::models::object::update)
.service(routes::models::object::delete)
//
.service(routes::property_model::create_form)
.service(routes::property_model::create)
.service(routes::property_model::update_form)
.service(routes::property_model::update)
.service(routes::property_model::delete)
.service(routes::object::create_form)
.service(routes::object::create)
.service(routes::models::relation::create_form)
.service(routes::models::relation::create)
.service(routes::models::relation::update_form)
.service(routes::models::relation::update)
.service(routes::models::relation::delete)
//
.service(routes::models::property::create_form)
.service(routes::models::property::create)
.service(routes::models::property::update_form)
.service(routes::models::property::update)
.service(routes::models::property::delete)
//
.service(routes::objects::list)
.service(routes::objects::create_form)
.service(routes::objects::create)
.service(static_files)
.default_service(web::to(|| HttpResponse::NotFound().body("File or endpoint not found")))

@ -13,75 +13,16 @@ use yopa::model::{ObjectModel, PropertyModel, RelationModel};
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::routes::relation_model::RelationModelDisplay;
use crate::routes::object_model::ObjectModelDisplay;
use crate::routes::models::relation::RelationModelDisplay;
use crate::routes::models::object::ObjectModelDisplay;
pub(crate) mod object_model;
pub(crate) mod relation_model;
pub(crate) mod property_model;
pub(crate) mod object;
pub(crate) mod models;
pub(crate) mod objects;
#[get("/")]
pub(crate) async fn index(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
let rg = store.read().await;
let models_iter = rg.get_object_models();
// object and relation props
let mut model_props = rg.get_grouped_prop_models();
let mut model_relations = rg.get_grouped_relation_models();
let mut model_rec_relations = rg.get_grouped_reciprocal_relation_models();
let mut models = vec![];
for om in models_iter {
let mut relations = model_relations.remove(&om.id).unwrap_or_default();
let mut relations = relations.into_iter().map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name);
RelationModelDisplay {
model: rm,
related_name: rg.get_model_name(rm.related),
properties: rprops,
}
}).collect::<Vec<_>>();
relations.sort_by_key(|d| &d.model.name);
// Relations coming INTO this model
let mut reciprocal_relations = model_rec_relations.remove(&om.id).unwrap_or_default();
let mut reciprocal_relations = reciprocal_relations.into_iter().map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name);
RelationModelDisplay {
model: rm,
related_name: rg.get_model_name(rm.object),
properties: rprops,
}
}).collect::<Vec<_>>();
reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name);
let mut properties = model_props.remove(&om.id).unwrap_or_default();
properties.sort_by_key(|m| &m.name);
models.push(ObjectModelDisplay {
id: om.id,
name: &om.name,
properties,
relations,
reciprocal_relations,
})
}
models.sort_by_key(|m| m.name);
let mut ctx = tera::Context::new();
ctx.insert("models", &models);
session.render_flash(&mut ctx);
TERA.build_response("models/schema", &ctx)
pub(crate) async fn index(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<HttpResponse> {
objects::list_inner(session, store)
.await
}
#[get("/takeout")]

@ -0,0 +1,73 @@
use actix_session::Session;
use actix_web::Responder;
use crate::routes::models::relation::RelationModelDisplay;
use crate::routes::models::object::ObjectModelDisplay;
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
pub(crate) mod object;
pub(crate) mod relation;
pub(crate) mod property;
#[get("/models")]
pub(crate) async fn list(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
let rg = store.read().await;
let models_iter = rg.get_object_models();
// object and relation props
let mut model_props = rg.get_grouped_prop_models();
let mut model_relations = rg.get_grouped_relation_models();
let mut model_rec_relations = rg.get_grouped_reciprocal_relation_models();
let mut models = vec![];
for om in models_iter {
let mut relations = model_relations.remove(&om.id).unwrap_or_default();
let mut relations = relations.into_iter().map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name);
RelationModelDisplay {
model: rm,
related_name: rg.get_model_name(rm.related),
properties: rprops,
}
}).collect::<Vec<_>>();
relations.sort_by_key(|d| &d.model.name);
// Relations coming INTO this model
let mut reciprocal_relations = model_rec_relations.remove(&om.id).unwrap_or_default();
let mut reciprocal_relations = reciprocal_relations.into_iter().map(|rm| {
let mut rprops = model_props.get(&rm.id).cloned().unwrap_or_default();
rprops.sort_by_key(|m| &m.name);
RelationModelDisplay {
model: rm,
related_name: rg.get_model_name(rm.object),
properties: rprops,
}
}).collect::<Vec<_>>();
reciprocal_relations.sort_by_key(|d| &d.model.reciprocal_name);
let mut properties = model_props.remove(&om.id).unwrap_or_default();
properties.sort_by_key(|m| &m.name);
models.push(ObjectModelDisplay {
id: om.id,
name: &om.name,
properties,
relations,
reciprocal_relations,
})
}
models.sort_by_key(|m| m.name);
let mut ctx = tera::Context::new();
ctx.insert("models", &models);
session.render_flash(&mut ctx);
TERA.build_response("models/index", &ctx)
}

@ -5,7 +5,7 @@ use serde::{Deserialize, Serialize};
use yopa::ID;
use yopa::model::{ObjectModel, PropertyModel};
use crate::routes::relation_model::RelationModelDisplay;
use crate::routes::models::relation::RelationModelDisplay;
use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
@ -55,7 +55,7 @@ pub(crate) async fn create(
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("Object model \"{}\" created.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error creating model: {}", e);
@ -109,7 +109,7 @@ pub(crate) async fn update(
Ok(_id) => {
debug!("Object updated, redirecting to root");
session.flash_success(format!("Object model \"{}\" updated.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error updating model: {}", e);
@ -132,7 +132,7 @@ pub(crate) async fn delete(
Ok(om) => {
debug!("Object model deleted, redirecting to root");
session.flash_success(format!("Object model \"{}\" deleted.", om.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error deleting object model: {}", e);

@ -9,7 +9,7 @@ use crate::session_ext::SessionExt;
use crate::TERA;
use crate::tera_ext::TeraExt;
use crate::utils::{ParseOrBadReq, redirect};
use crate::routes::relation_model::ObjectOrRelationModelDisplay;
use crate::routes::models::relation::ObjectOrRelationModelDisplay;
#[get("/model/property/create/{object_id}")]
pub(crate) async fn create_form(
@ -148,7 +148,7 @@ pub(crate) async fn create(
Ok(_id) => {
debug!("Property created, redirecting to root");
session.flash_success(format!("Property model \"{}\" created.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error creating property model: {}", e);
@ -170,12 +170,12 @@ pub(crate) async fn delete(
Ok(rm) => {
debug!("Property deleted, redirecting to root");
session.flash_success(format!("Property \"{}\" deleted.", rm.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error deleting property: {}", e);
session.flash_error(e.to_string());
redirect("/") // back?
redirect("/models") // back?
}
}
}
@ -256,7 +256,7 @@ pub(crate) async fn update(
Ok(_id) => {
debug!("Relation updated, redirecting to root");
session.flash_success(format!("Property \"{}\" updated.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error updating model: {}", e);

@ -53,8 +53,6 @@ pub(crate) async fn create_form(
context.insert("models", &models);
context.insert("object", &object);
TERA.build_response("models/relation_create", &context)
}
@ -90,7 +88,7 @@ pub(crate) async fn create(
Ok(_id) => {
debug!("Relation created, redirecting to root");
session.flash_success(format!("Relation model \"{}\" created.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error creating relation model: {}", e);
@ -118,12 +116,12 @@ pub(crate) async fn delete(
Ok(rm) => {
debug!("Relation deleted, redirecting to root");
session.flash_success(format!("Relation model \"{}\" deleted.", rm.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error deleting relation model: {}", e);
session.flash_error(e.to_string());
redirect("/") // back?
redirect("/models") // back?
}
}
}
@ -188,7 +186,7 @@ pub(crate) async fn update(
Ok(_id) => {
debug!("Relation updated, redirecting to root");
session.flash_success(format!("Relation model \"{}\" updated.", form.name));
redirect("/")
redirect("/models")
}
Err(e) => {
warn!("Error updating model: {}", e);

@ -1,7 +1,7 @@
use actix_session::Session;
use actix_web::{Responder, web, HttpResponse};
use crate::session_ext::SessionExt;
use crate::routes::object_model::ObjectModelForm;
use crate::routes::models::object::ObjectModelForm;
use crate::TERA;
use crate::tera_ext::TeraExt;
use yopa::{ID, model};
@ -11,6 +11,9 @@ use yopa::insert::InsertObj;
use crate::utils::redirect;
use actix_web::web::Json;
use serde_json::Value;
use itertools::Itertools;
use yopa::model::ObjectModel;
use heck::TitleCase;
// we only need references here, Context serializes everything to Value.
// cloning would be a waste of cycles
@ -85,13 +88,17 @@ pub(crate) async fn create(
//
// Ok(HttpResponse::Ok().finish())
//
let mut wg = store.write().await;
let form = form.into_inner();
let name = form.name.clone();
let model_name = wg.get_model_name(form.model_id).to_owned().to_title_case();
match wg.insert_object(form) {
Ok(_id) => {
debug!("Object created, redirecting to root");
session.flash_success(format!("Object \"{}\" created.", name));
session.flash_success(format!("{} \"{}\" created.", model_name, name));
Ok(HttpResponse::Ok().finish())
}
Err(e) => {
@ -100,3 +107,37 @@ pub(crate) async fn create(
}
}
}
#[derive(Debug, Serialize, Clone)]
struct ModelWithObjects<'a> {
model : &'a ObjectModel,
objects: Vec<&'a Object>
}
#[get("/objects")]
pub(crate) async fn list(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<impl Responder> {
list_inner(session, store).await
}
pub(crate) async fn list_inner(session: Session, store: crate::YopaStoreWrapper) -> actix_web::Result<HttpResponse> {
let rg = store.read().await;
let mut objects_by_model = rg.get_grouped_objects();
let mut models : Vec<_> = rg.get_object_models()
.sorted_by_key(|m| &m.name)
.map(|model| {
let mut objects = objects_by_model.remove(&model.id).unwrap_or_default();
objects.sort_by_key(|o| &o.name);
ModelWithObjects {
model,
objects
}
}).collect();
let mut ctx = tera::Context::new();
ctx.insert("models", &models);
session.render_flash(&mut ctx);
TERA.build_response("objects/index", &ctx)
}

@ -53,7 +53,7 @@ pub struct Storage {
pub enum StorageError {
#[error("Referenced {0} does not exist")]
NotExist(Cow<'static, str>),
#[error("Schema constraint violation: {0}")]
#[error("{0}")]
ConstraintViolation(Cow<'static, str>),
}
@ -66,11 +66,11 @@ impl Storage {
/// Define a data object
pub fn define_object(&mut self, mut tpl: model::ObjectModel) -> Result<ID, StorageError> {
if tpl.name.is_empty() {
return Err(StorageError::ConstraintViolation("name must not be empty".into()));
return Err(StorageError::ConstraintViolation("Name must not be empty".into()));
}
if self.obj_models.iter().find(|(_, t)| t.name == tpl.name).is_some() {
return Err(StorageError::ConstraintViolation(format!("object model with the name \"{}\" already exists", tpl.name).into()));
return Err(StorageError::ConstraintViolation(format!("Object model with the name \"{}\" already exists", tpl.name).into()));
}
debug!("Define object model \"{}\"", tpl.name);
@ -83,14 +83,14 @@ impl Storage {
/// Define a relation between two data objects
pub fn define_relation(&mut self, mut rel: model::RelationModel) -> Result<ID, StorageError> {
if rel.name.is_empty() || rel.reciprocal_name.is_empty() {
return Err(StorageError::ConstraintViolation("names must not be empty".into()));
return Err(StorageError::ConstraintViolation("Names must not be empty".into()));
}
if !self.obj_models.contains_key(&rel.object) {
return Err(StorageError::NotExist(format!("source object model {}", rel.object).into()));
return Err(StorageError::NotExist(format!("Source object model {}", rel.object).into()));
}
if !self.obj_models.contains_key(&rel.related) {
return Err(StorageError::NotExist(format!("related object model {}", rel.related).into()));
return Err(StorageError::NotExist(format!("Related object model {}", rel.related).into()));
}
if let Some((_, colliding)) = self.rel_models.iter().find(|(_, other)| {
@ -100,7 +100,7 @@ impl Storage {
|| (other.reciprocal_name == rel.reciprocal_name && other.related == rel.related) // Reciprocal names collide for the same destination
}) {
return Err(StorageError::ConstraintViolation(
format!("name collision (\"{}\" / \"{}\") with existing relation (\"{}\" / \"{}\")",
format!("Name collision (\"{}\" / \"{}\") with existing relation (\"{}\" / \"{}\")",
rel.name, rel.reciprocal_name,
colliding.name, colliding.reciprocal_name
).into()));
@ -118,19 +118,19 @@ impl Storage {
/// Define a property attached to an object or a relation
pub fn define_property(&mut self, mut prop: model::PropertyModel) -> Result<ID, StorageError> {
if prop.name.is_empty() {
return Err(StorageError::ConstraintViolation("name must not be empty".into()));
return Err(StorageError::ConstraintViolation("Name must not be empty".into()));
}
if !self.obj_models.contains_key(&prop.object) {
// Maybe it's attached to a relation?
if !self.rel_models.contains_key(&prop.object) {
return Err(StorageError::NotExist(format!("object or relation model {}", prop.object).into()));
return Err(StorageError::NotExist(format!("Object or relation model {}", prop.object).into()));
}
}
if self.prop_models.iter().find(|(_, t)| t.object == prop.object && t.name == prop.name).is_some() {
return Err(StorageError::ConstraintViolation(
format!("property with the name \"{}\" already exists on model {}", prop.name, self.describe_model(prop.object)).into()));
format!("Property with the name \"{}\" already exists on model {}", prop.name, self.describe_model(prop.object)).into()));
}
// Ensure the default type is compatible
@ -245,6 +245,12 @@ impl Storage {
None => return Err(StorageError::NotExist(format!("object model {}", obj_model_id).into()))
};
// 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 = next_id();
let object = data::Object {
id: object_id,
@ -377,6 +383,11 @@ impl Storage {
.filter(move |object| model_ids.contains(&object.model))
}
pub fn get_grouped_objects(&self) -> HashMap<ID, Vec<&Object>> {
self.objects.values()
.into_group_map_by(|object| object.model)
}
pub fn get_grouped_relation_models(&self) -> HashMap<ID, Vec<&RelationModel>> {
self.rel_models.values()
.into_group_map_by(|model| model.object)

Loading…
Cancel
Save