Skip to content

Commit c2e00aa

Browse files
committed
run_project
1 parent 07b2671 commit c2e00aa

File tree

12 files changed

+141
-28
lines changed

12 files changed

+141
-28
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

lib/api_run/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ sentry = ["bencher_schema/sentry"]
1717
[dependencies]
1818
bencher_endpoint.workspace = true
1919
bencher_json = { workspace = true, features = ["server", "schema", "db"] }
20+
bencher_rbac.workspace = true
2021
bencher_schema.workspace = true
2122
dropshot.workspace = true
2223
slog.workspace = true

lib/api_run/src/run.rs

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use bencher_endpoint::{CorsResponse, Endpoint, Post, ResponseCreated};
2-
use bencher_json::{JsonNewRun, JsonReport};
2+
use bencher_json::{project, system::auth, JsonNewRun, JsonReport, NameIdKind, ResourceName};
33
use bencher_schema::{
4+
conn_lock,
45
context::ApiContext,
5-
error::bad_request_error,
6+
error::{bad_request_error, forbidden_error},
67
model::{
8+
organization::QueryOrganization,
79
project::{report::QueryReport, QueryProject},
810
user::auth::{AuthUser, PubBearerToken},
911
},
@@ -47,11 +49,13 @@ async fn post_inner(
4749
json_run: JsonNewRun,
4850
auth_user: Option<AuthUser>,
4951
) -> Result<JsonReport, HttpError> {
50-
#[allow(clippy::unimplemented)]
51-
let todo_pub_run_project = || -> Result<QueryProject, HttpError> {
52-
Err(bad_request_error("pub run creation is not yet implemented"))
53-
};
54-
let query_project = todo_pub_run_project()?;
52+
let query_project = QueryProject::get_or_create(
53+
context,
54+
json_run.organization.as_ref(),
55+
json_run.project.as_ref(),
56+
auth_user.as_ref(),
57+
)
58+
.await?;
5559
#[allow(clippy::unimplemented)]
5660
let todo_pub_run_user = |_auth_user: Option<AuthUser>| -> Result<AuthUser, HttpError> {
5761
Err(bad_request_error("pub run creation is not yet implemented"))

lib/bencher_schema/src/macros/fn_get.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -73,24 +73,27 @@ macro_rules! fn_get_uuid {
7373
pub(crate) use fn_get_uuid;
7474

7575
macro_rules! fn_from_uuid {
76-
($table:ident, $uuid:ident, $resource:ident) => {
76+
($parent:ident, $parent_type:ty, $table:ident, $uuid:ident, $resource:ident) => {
7777
#[allow(unused_qualifications)]
7878
pub fn from_uuid(
7979
conn: &mut DbConnection,
80-
project_id: ProjectId,
80+
parent: $parent_type,
8181
uuid: $uuid,
8282
) -> Result<Self, HttpError> {
8383
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
8484
crate::schema::$table::table
85-
.filter(crate::schema::$table::project_id.eq(project_id))
85+
.filter(crate::schema::$table::$parent.eq(parent))
8686
.filter(crate::schema::$table::uuid.eq(uuid))
8787
.first::<Self>(conn)
8888
.map_err($crate::error::resource_not_found_err!(
8989
$resource,
90-
(project_id, uuid)
90+
(parent, uuid)
9191
))
9292
}
9393
};
94+
($table:ident, $uuid:ident, $resource:ident) => {
95+
fn_from_uuid!(project_id, ProjectId, $table, $uuid, $resource);
96+
};
9497
}
9598

9699
pub(crate) use fn_from_uuid;

lib/bencher_schema/src/macros/resource_id.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -36,36 +36,35 @@ macro_rules! fn_eq_resource_id {
3636
pub(crate) use fn_eq_resource_id;
3737

3838
macro_rules! fn_from_resource_id {
39-
// The `root` parameter is just a kludge to distinguish between top level and project level resources
40-
($table:ident, $resource:ident, $root:expr) => {
39+
($parent:ident, $parent_type:ty, $table:ident, $resource:ident) => {
4140
#[allow(unused_qualifications)]
4241
pub fn from_resource_id(
4342
conn: &mut crate::context::DbConnection,
43+
parent: $parent_type,
4444
resource_id: &bencher_json::ResourceId,
4545
) -> Result<Self, HttpError> {
4646
schema::$table::table
47+
.filter(schema::$table::$parent.eq(parent))
4748
.filter(Self::eq_resource_id(resource_id)?)
4849
.first::<Self>(conn)
4950
.map_err(crate::error::resource_not_found_err!(
5051
$resource,
51-
resource_id
52+
(parent, resource_id)
5253
))
5354
}
5455
};
5556
($table:ident, $resource:ident) => {
5657
#[allow(unused_qualifications)]
5758
pub fn from_resource_id(
5859
conn: &mut crate::context::DbConnection,
59-
project_id: crate::model::project::ProjectId,
6060
resource_id: &bencher_json::ResourceId,
6161
) -> Result<Self, HttpError> {
6262
schema::$table::table
63-
.filter(schema::$table::project_id.eq(project_id))
6463
.filter(Self::eq_resource_id(resource_id)?)
6564
.first::<Self>(conn)
6665
.map_err(crate::error::resource_not_found_err!(
6766
$resource,
68-
(project_id, resource_id)
67+
resource_id
6968
))
7069
}
7170
};

lib/bencher_schema/src/model/organization/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ pub struct QueryOrganization {
4141

4242
impl QueryOrganization {
4343
fn_eq_resource_id!(organization);
44-
fn_from_resource_id!(organization, Organization, true);
44+
fn_from_resource_id!(organization, Organization);
4545

4646
fn_get!(organization, OrganizationId);
4747
fn_get_id!(organization, OrganizationId, OrganizationUuid);

lib/bencher_schema/src/model/project/benchmark.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ pub struct QueryBenchmark {
3838

3939
impl QueryBenchmark {
4040
fn_eq_resource_id!(benchmark);
41-
fn_from_resource_id!(benchmark, Benchmark);
41+
fn_from_resource_id!(project_id, ProjectId, benchmark, Benchmark);
4242

4343
fn_get!(benchmark, BenchmarkId);
4444
fn_get_id!(benchmark, BenchmarkId, BenchmarkUuid);

lib/bencher_schema/src/model/project/branch/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ pub struct QueryBranch {
5454

5555
impl QueryBranch {
5656
fn_eq_resource_id!(branch);
57-
fn_from_resource_id!(branch, Branch);
57+
fn_from_resource_id!(project_id, ProjectId, branch, Branch);
5858

5959
fn_eq_name_id!(BranchName, branch);
6060
fn_from_name_id!(branch, Branch);

lib/bencher_schema/src/model/project/measure.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ pub struct QueryMeasure {
4545

4646
impl QueryMeasure {
4747
fn_eq_resource_id!(measure);
48-
fn_from_resource_id!(measure, Measure);
48+
fn_from_resource_id!(project_id, ProjectId, measure, Measure);
4949

5050
fn_eq_name_id!(ResourceName, measure);
5151
fn_from_name_id!(measure, Measure);

lib/bencher_schema/src/model/project/mod.rs

Lines changed: 110 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,29 @@ use std::string::ToString;
22

33
use bencher_json::{
44
project::{JsonProjectPatch, JsonProjectPatchNull, JsonUpdateProject, Visibility},
5-
DateTime, JsonNewProject, JsonProject, ProjectUuid, ResourceId, ResourceName, Slug, Url,
5+
DateTime, JsonNewProject, JsonProject, NameId, NameIdKind, ProjectUuid, ResourceId,
6+
ResourceName, Slug, Url,
67
};
78
use bencher_rbac::{project::Permission, Organization, Project};
89
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl};
910
use dropshot::HttpError;
1011

1112
use crate::{
13+
conn_lock,
1214
context::{DbConnection, Rbac},
1315
error::{
14-
assert_parentage, forbidden_error, resource_not_found_error, unauthorized_error,
15-
BencherResource,
16+
assert_parentage, bad_request_error, conflict_error, forbidden_error,
17+
resource_conflict_err, resource_not_found_err, resource_not_found_error,
18+
unauthorized_error, BencherResource,
1619
},
1720
macros::{
18-
fn_get::{fn_get, fn_get_uuid},
21+
fn_get::{fn_from_uuid, fn_get, fn_get_uuid},
1922
resource_id::{fn_eq_resource_id, fn_from_resource_id},
2023
slug::ok_slug,
2124
},
2225
model::{organization::QueryOrganization, user::auth::AuthUser},
2326
schema::{self, project as project_table},
27+
ApiContext,
2428
};
2529

2630
use super::organization::OrganizationId;
@@ -57,10 +61,111 @@ pub struct QueryProject {
5761

5862
impl QueryProject {
5963
fn_eq_resource_id!(project);
60-
fn_from_resource_id!(project, Project, true);
64+
fn_from_resource_id!(project, Project);
6165

6266
fn_get!(project, ProjectId);
6367
fn_get_uuid!(project, ProjectId, ProjectUuid);
68+
fn_from_uuid!(
69+
organization_id,
70+
OrganizationId,
71+
project,
72+
ProjectUuid,
73+
Project
74+
);
75+
76+
pub async fn get_or_create(
77+
context: &ApiContext,
78+
organization: &ResourceId,
79+
project: &NameId,
80+
auth_user: &AuthUser,
81+
) -> Result<ProjectId, HttpError> {
82+
let query_organization =
83+
QueryOrganization::from_resource_id(conn_lock!(context), organization)?;
84+
let query_project =
85+
Self::get_or_create_inner(context, &query_organization, project, auth_user).await?;
86+
Ok(query_project.id)
87+
}
88+
89+
async fn get_or_create_inner(
90+
context: &ApiContext,
91+
query_organization: &QueryOrganization,
92+
project: &NameId,
93+
auth_user: &AuthUser,
94+
) -> Result<Self, HttpError> {
95+
let Ok(kind) = NameIdKind::<ResourceName>::try_from(project) else {
96+
return Err(bad_request_error(format!(
97+
"Project ({project}) must be a valid UUID, slug, or name"
98+
)));
99+
};
100+
let query_project = match kind {
101+
NameIdKind::Uuid(uuid) => {
102+
QueryProject::from_uuid(conn_lock!(context), query_organization.id, uuid.into())?
103+
},
104+
NameIdKind::Slug(slug) => {
105+
if let Ok(query_project) = schema::project::table
106+
.filter(schema::project::organization_id.eq(query_organization.id))
107+
.filter(schema::project::slug.eq(&slug))
108+
.first::<Self>(conn_lock!(context))
109+
{
110+
query_project
111+
} else {
112+
let new_project = JsonNewProject {
113+
name: slug.clone().into(),
114+
slug: Some(slug.clone()),
115+
url: None,
116+
visibility: None,
117+
};
118+
Self::create(context, query_organization, new_project, auth_user).await?
119+
}
120+
},
121+
NameIdKind::Name(name) => {
122+
if let Ok(query_project) = schema::project::table
123+
.filter(schema::project::organization_id.eq(query_organization.id))
124+
.filter(schema::project::name.eq(&name))
125+
.first::<Self>(conn_lock!(context))
126+
{
127+
query_project
128+
} else {
129+
let new_project = JsonNewProject {
130+
name,
131+
slug: None,
132+
url: None,
133+
visibility: None,
134+
};
135+
Self::create(context, query_organization, new_project, auth_user).await?
136+
}
137+
},
138+
};
139+
140+
Ok(query_project)
141+
}
142+
143+
async fn create(
144+
context: &ApiContext,
145+
query_organization: &QueryOrganization,
146+
new_project: JsonNewProject,
147+
auth_user: &AuthUser,
148+
) -> Result<Self, HttpError> {
149+
let insert_project =
150+
InsertProject::from_json(conn_lock!(context), query_organization, new_project)?;
151+
152+
// Check to see if user has permission to create a project within the organization
153+
context
154+
.rbac
155+
.is_allowed_organization(
156+
auth_user,
157+
bencher_rbac::organization::Permission::Create,
158+
&insert_project,
159+
)
160+
.map_err(forbidden_error)?;
161+
162+
let conn = conn_lock!(context);
163+
diesel::insert_into(project_table::table)
164+
.values(&insert_project)
165+
.execute(conn)
166+
.map_err(resource_conflict_err!(Project, &insert_project))?;
167+
Self::from_uuid(conn, query_organization.id, insert_project.uuid)
168+
}
64169

65170
pub fn is_public(&self) -> bool {
66171
self.visibility.is_public()

0 commit comments

Comments
 (0)