diff --git a/rfd-api-spec.json b/rfd-api-spec.json index 38cbe202..baccda4b 100644 --- a/rfd-api-spec.json +++ b/rfd-api-spec.json @@ -1941,7 +1941,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RfdRevisionPdf" + "$ref": "#/components/schemas/RfdWithPdf" } } } @@ -1976,7 +1976,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RfdWithContent" + "$ref": "#/components/schemas/RfdWithRaw" } } } @@ -2212,7 +2212,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RfdRevisionPdf" + "$ref": "#/components/schemas/RfdWithPdf" } } } @@ -2256,7 +2256,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/RfdWithContent" + "$ref": "#/components/schemas/RfdWithRaw" } } } @@ -3677,7 +3677,12 @@ "type": "object", "properties": { "content": { - "$ref": "#/components/schemas/RfdRevision" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/RfdRevision" + } + ] }, "created_at": { "type": "string", @@ -3708,7 +3713,6 @@ } }, "required": [ - "content", "created_at", "id", "rfd_number", @@ -4407,80 +4411,6 @@ "updated_at" ] }, - "RfdRevisionPdf": { - "type": "object", - "properties": { - "authors": { - "nullable": true, - "type": "string" - }, - "commit": { - "$ref": "#/components/schemas/CommitSha" - }, - "committed_at": { - "type": "string", - "format": "date-time" - }, - "content": { - "type": "array", - "items": { - "$ref": "#/components/schemas/RfdPdf" - } - }, - "content_format": { - "$ref": "#/components/schemas/ContentFormat" - }, - "created_at": { - "type": "string", - "format": "date-time" - }, - "deleted_at": { - "nullable": true, - "type": "string", - "format": "date-time" - }, - "discussion": { - "nullable": true, - "type": "string" - }, - "id": { - "$ref": "#/components/schemas/TypedUuidForRfdRevisionId" - }, - "labels": { - "nullable": true, - "type": "string" - }, - "rfd_id": { - "$ref": "#/components/schemas/TypedUuidForRfdId" - }, - "sha": { - "$ref": "#/components/schemas/FileSha" - }, - "state": { - "nullable": true, - "type": "string" - }, - "title": { - "type": "string" - }, - "updated_at": { - "type": "string", - "format": "date-time" - } - }, - "required": [ - "commit", - "committed_at", - "content", - "content_format", - "created_at", - "id", - "rfd_id", - "sha", - "title", - "updated_at" - ] - }, "RfdState": { "type": "string", "enum": [ @@ -4537,7 +4467,7 @@ "visibility" ] }, - "RfdWithContent": { + "RfdWithPdf": { "type": "object", "properties": { "authors": { @@ -4545,21 +4475,35 @@ "type": "string" }, "commit": { - "$ref": "#/components/schemas/CommitSha" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/CommitSha" + } + ] }, "committed_at": { + "nullable": true, "type": "string", "format": "date-time" }, "content": { - "type": "string" + "type": "array", + "items": { + "$ref": "#/components/schemas/RfdPdf" + } }, "discussion": { "nullable": true, "type": "string" }, "format": { - "$ref": "#/components/schemas/ContentFormat" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ContentFormat" + } + ] }, "id": { "$ref": "#/components/schemas/TypedUuidForRfdId" @@ -4577,13 +4521,19 @@ "format": "int32" }, "sha": { - "$ref": "#/components/schemas/FileSha" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/FileSha" + } + ] }, "state": { "nullable": true, "type": "string" }, "title": { + "nullable": true, "type": "string" }, "visibility": { @@ -4591,14 +4541,86 @@ } }, "required": [ - "commit", - "committed_at", "content", - "format", "id", "rfd_number", - "sha", - "title", + "visibility" + ] + }, + "RfdWithRaw": { + "type": "object", + "properties": { + "authors": { + "nullable": true, + "type": "string" + }, + "commit": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/CommitSha" + } + ] + }, + "committed_at": { + "nullable": true, + "type": "string", + "format": "date-time" + }, + "content": { + "nullable": true, + "type": "string" + }, + "discussion": { + "nullable": true, + "type": "string" + }, + "format": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ContentFormat" + } + ] + }, + "id": { + "$ref": "#/components/schemas/TypedUuidForRfdId" + }, + "labels": { + "nullable": true, + "type": "string" + }, + "link": { + "nullable": true, + "type": "string" + }, + "rfd_number": { + "type": "integer", + "format": "int32" + }, + "sha": { + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/FileSha" + } + ] + }, + "state": { + "nullable": true, + "type": "string" + }, + "title": { + "nullable": true, + "type": "string" + }, + "visibility": { + "$ref": "#/components/schemas/Visibility" + } + }, + "required": [ + "id", + "rfd_number", "visibility" ] }, @@ -4610,9 +4632,15 @@ "type": "string" }, "commit": { - "$ref": "#/components/schemas/CommitSha" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/CommitSha" + } + ] }, "committed_at": { + "nullable": true, "type": "string", "format": "date-time" }, @@ -4621,7 +4649,12 @@ "type": "string" }, "format": { - "$ref": "#/components/schemas/ContentFormat" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/ContentFormat" + } + ] }, "id": { "$ref": "#/components/schemas/TypedUuidForRfdId" @@ -4639,13 +4672,19 @@ "format": "int32" }, "sha": { - "$ref": "#/components/schemas/FileSha" + "nullable": true, + "allOf": [ + { + "$ref": "#/components/schemas/FileSha" + } + ] }, "state": { "nullable": true, "type": "string" }, "title": { + "nullable": true, "type": "string" }, "visibility": { @@ -4653,13 +4692,8 @@ } }, "required": [ - "commit", - "committed_at", - "format", "id", "rfd_number", - "sha", - "title", "visibility" ] }, diff --git a/rfd-api/src/context.rs b/rfd-api/src/context.rs index 517eba1e..1db570dc 100644 --- a/rfd-api/src/context.rs +++ b/rfd-api/src/context.rs @@ -32,7 +32,10 @@ use tap::TapFallible; use thiserror::Error; use tracing::instrument; use v_api::{ - response::{resource_not_found, resource_restricted, ResourceResult, ToResourceResult}, + response::{ + resource_not_found, resource_restricted, ResourceResult, ToResourceResult, + ToResourceResultOpt, + }, ApiContext, VContext, }; use v_model::{ @@ -97,17 +100,17 @@ pub struct RfdWithRaw { pub rfd_number: i32, pub link: Option, pub discussion: Option, - pub title: String, + pub title: Option, pub state: Option, pub authors: Option, pub labels: Option, #[partial(RfdWithoutContent(skip))] #[partial(RfdWithPdf(retype = Vec))] - pub content: String, - pub format: ContentFormat, - pub sha: FileSha, - pub commit: CommitSha, - pub committed_at: DateTime, + pub content: Option, + pub format: Option, + pub sha: Option, + pub commit: Option, + pub committed_at: Option>, pub visibility: Visibility, } @@ -117,16 +120,37 @@ impl From for RfdWithRaw { id: value.id, rfd_number: value.rfd_number, link: value.link, - discussion: value.content.discussion, - title: value.content.title, - state: value.content.state, - authors: value.content.authors, - labels: value.content.labels, - content: value.content.content, - format: value.content.content_format, - sha: value.content.sha, - commit: value.content.commit.into(), - committed_at: value.content.committed_at, + discussion: value + .content + .as_ref() + .and_then(|content| content.discussion.clone()), + title: value.content.as_ref().map(|content| content.title.clone()), + state: value + .content + .as_ref() + .and_then(|content| content.state.clone()), + authors: value + .content + .as_ref() + .and_then(|content| content.authors.clone()), + labels: value + .content + .as_ref() + .and_then(|content| content.labels.clone()), + content: value + .content + .as_ref() + .map(|content| content.content.clone()), + format: value + .content + .as_ref() + .map(|content| content.content_format.clone()), + sha: value.content.as_ref().map(|content| content.sha.clone()), + commit: value.content.as_ref().map(|content| content.commit.clone()), + committed_at: value + .content + .as_ref() + .map(|content| content.committed_at.clone()), visibility: value.visibility, } } @@ -138,15 +162,33 @@ impl From for RfdWithoutContent { id: value.id, rfd_number: value.rfd_number, link: value.link, - discussion: value.content.discussion, - title: value.content.title, - state: value.content.state, - authors: value.content.authors, - labels: value.content.labels, - format: value.content.content_format, - sha: value.content.sha, - commit: value.content.commit.into(), - committed_at: value.content.committed_at, + discussion: value + .content + .as_ref() + .and_then(|content| content.discussion.clone()), + title: value.content.as_ref().map(|content| content.title.clone()), + state: value + .content + .as_ref() + .and_then(|content| content.state.clone()), + authors: value + .content + .as_ref() + .and_then(|content| content.authors.clone()), + labels: value + .content + .as_ref() + .and_then(|content| content.labels.clone()), + format: value + .content + .as_ref() + .map(|content| content.content_format.clone()), + sha: value.content.as_ref().map(|content| content.sha.clone()), + commit: value.content.as_ref().map(|content| content.commit.clone()), + committed_at: value + .content + .as_ref() + .map(|content| content.committed_at.clone()), visibility: value.visibility, } } @@ -158,16 +200,38 @@ impl From for RfdWithPdf { id: value.id, rfd_number: value.rfd_number, link: value.link, - discussion: value.content.discussion, - title: value.content.title, - state: value.content.state, - authors: value.content.authors, - labels: value.content.labels, - content: value.content.content, - format: value.content.content_format, - sha: value.content.sha, - commit: value.content.commit.into(), - committed_at: value.content.committed_at, + discussion: value + .content + .as_ref() + .and_then(|content| content.discussion.clone()), + title: value.content.as_ref().map(|content| content.title.clone()), + state: value + .content + .as_ref() + .and_then(|content| content.state.clone()), + authors: value + .content + .as_ref() + .and_then(|content| content.authors.clone()), + labels: value + .content + .as_ref() + .and_then(|content| content.labels.clone()), + content: value + .content + .as_ref() + .map(|content| content.content.clone()) + .unwrap_or_default(), + format: value + .content + .as_ref() + .map(|content| content.content_format.clone()), + sha: value.content.as_ref().map(|content| content.sha.clone()), + commit: value.content.as_ref().map(|content| content.commit.clone()), + committed_at: value + .content + .as_ref() + .map(|content| content.committed_at.clone()), visibility: value.visibility, } } @@ -381,21 +445,7 @@ impl RfdContext { let mut rfd_list = rfds .into_iter() - .map(|rfd| RfdWithoutContent { - id: rfd.id, - rfd_number: rfd.rfd_number, - link: rfd.link, - discussion: rfd.content.discussion, - title: rfd.content.title, - state: rfd.content.state, - authors: rfd.content.authors, - labels: rfd.content.labels, - format: rfd.content.content_format, - sha: rfd.content.sha, - commit: rfd.content.commit.into(), - committed_at: rfd.content.committed_at, - visibility: rfd.visibility, - }) + .map(|rfd| RfdWithoutContent::from(rfd)) .collect::>(); // Finally sort the RFD list by RFD number @@ -533,7 +583,7 @@ impl RfdContext { revision: Option, ) -> ResourceResult { let rfd = self.get_rfd(caller, rfd_number, revision).await?; - Ok(rfd.content) + Ok(rfd.content).opt_to_resource_result() } async fn get_latest_rfd_revision( diff --git a/rfd-api/src/endpoints/rfd.rs b/rfd-api/src/endpoints/rfd.rs index 16968bc3..602d432f 100644 --- a/rfd-api/src/endpoints/rfd.rs +++ b/rfd-api/src/endpoints/rfd.rs @@ -19,7 +19,7 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use trace_request::trace_request; use tracing::instrument; -use v_api::ApiContext; +use v_api::{response::not_found, ApiContext}; use v_model::permissions::Caller; use crate::{ @@ -407,9 +407,14 @@ async fn view_rfd_attr_op( ) -> Result, HttpError> { if let Ok(rfd_number) = number.parse::() { let rfd = ctx.view_rfd(caller, rfd_number, None).await?; - let content = match rfd.format { - ContentFormat::Asciidoc => RfdContent::Asciidoc(RfdAsciidoc::new(rfd.content)), - ContentFormat::Markdown => RfdContent::Markdown(RfdMarkdown::new(rfd.content)), + let content = match (rfd.content, rfd.format) { + (Some(content), Some(ContentFormat::Asciidoc)) => { + RfdContent::Asciidoc(RfdAsciidoc::new(content)) + } + (Some(content), Some(ContentFormat::Markdown)) => { + RfdContent::Markdown(RfdMarkdown::new(content)) + } + _ => Err(not_found("RFD does not have any assigned revisions"))?, }; extract_attr(&attr, &content).map(HttpResponseOk) @@ -900,7 +905,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(private_rfd_id_1), rfd_number: 123, link: None, - content: RfdRevision { + content: Some(RfdRevision { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -916,7 +921,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, @@ -926,7 +931,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(public_rfd_id), rfd_number: 456, link: None, - content: RfdRevision { + content: Some(RfdRevision { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -942,7 +947,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, @@ -952,7 +957,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(private_rfd_id_2), rfd_number: 789, link: None, - content: RfdRevision { + content: Some(RfdRevision { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -968,7 +973,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, @@ -996,7 +1001,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(private_rfd_id_1), rfd_number: 123, link: None, - content: RfdRevisionMeta { + content: Some(RfdRevisionMeta { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -1011,7 +1016,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, @@ -1021,7 +1026,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(public_rfd_id), rfd_number: 456, link: None, - content: RfdRevisionMeta { + content: Some(RfdRevisionMeta { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -1036,7 +1041,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, @@ -1046,7 +1051,7 @@ mod tests { id: TypedUuid::from_untyped_uuid(private_rfd_id_2), rfd_number: 789, link: None, - content: RfdRevisionMeta { + content: Some(RfdRevisionMeta { id: TypedUuid::new_v4(), rfd_id: TypedUuid::from_untyped_uuid(private_rfd_id_1), title: String::new(), @@ -1061,7 +1066,7 @@ mod tests { created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, - }, + }), created_at: Utc::now(), updated_at: Utc::now(), deleted_at: None, diff --git a/rfd-cli/src/main.rs b/rfd-cli/src/main.rs index a1abadf5..1b6c6506 100644 --- a/rfd-cli/src/main.rs +++ b/rfd-cli/src/main.rs @@ -162,7 +162,7 @@ fn cmd_path<'a>(cmd: &CliCommand) -> Option<&'a str> { CliCommand::GetApiUserToken => Some("sys user token get"), CliCommand::ListApiUserTokens => Some("sys user token list"), CliCommand::UpdateApiUser => Some("sys user update"), - CliCommand::GetSelf => Some("self"), + CliCommand::GetSelf => Some("sys user self"), // Set user email CliCommand::SetApiUserContactEmail => Some("sys user contact email set"), @@ -418,7 +418,9 @@ impl ProgenitorCliConfig for Context { .printer() .unwrap() .output_oauth_secret(reserialize(value)), - "Array_of_RfdMeta" => self.printer().unwrap().output_rfd_list(reserialize(value)), + "Array_of_RfdWithoutContent" => { + self.printer().unwrap().output_rfd_list(reserialize(value)) + } "FullRfd" => self.printer().unwrap().output_rfd_full(reserialize(value)), "Rfd" => self.printer().unwrap().output_rfd(reserialize(value)), "SearchResults" => self diff --git a/rfd-model/src/db.rs b/rfd-model/src/db.rs index 33006841..6cb6c0b3 100644 --- a/rfd-model/src/db.rs +++ b/rfd-model/src/db.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ schema::{job, rfd, rfd_pdf, rfd_revision}, - schema_ext::{rfd_pdfs, ContentFormat, PdfSource, Visibility}, + schema_ext::{rfd_meta_join, rfd_pdf_join, ContentFormat, PdfSource, Visibility}, }; #[derive(Debug, Deserialize, Serialize, Queryable, Insertable, Selectable)] @@ -25,9 +25,62 @@ pub struct RfdModel { pub visibility: Visibility, } +// #[derive(QueryableByName)] +// #[diesel(table_name = rfd_join)] +// pub struct RfdJoinRow { +// pub id: Uuid, +// pub rfd_number: i32, +// pub link: Option, +// pub created_at: DateTime, +// pub updated_at: DateTime, +// pub deleted_at: Option>, +// pub visibility: Visibility, +// pub revision_id: Uuid, +// pub revision_rfd_id: Uuid, +// pub revision_title: String, +// pub revision_state: Option, +// pub revision_discussion: Option, +// pub revision_authors: Option, +// pub revision_content: String, +// pub revision_content_format: ContentFormat, +// pub revision_sha: String, +// pub revision_commit_sha: String, +// pub revision_committed_at: DateTime, +// pub revision_created_at: DateTime, +// pub revision_updated_at: DateTime, +// pub revision_deleted_at: Option>, +// pub revision_labels: Option, +// } + #[derive(QueryableByName)] -#[diesel(table_name = rfd_pdfs)] -pub struct RfdPdfsRow { +#[diesel(table_name = rfd_meta_join)] +pub struct RfdMetaJoinRow { + pub id: Uuid, + pub rfd_number: i32, + pub link: Option, + pub created_at: DateTime, + pub updated_at: DateTime, + pub deleted_at: Option>, + pub visibility: Visibility, + pub revision_id: Uuid, + pub revision_rfd_id: Uuid, + pub revision_title: String, + pub revision_state: Option, + pub revision_discussion: Option, + pub revision_authors: Option, + pub revision_content_format: ContentFormat, + pub revision_sha: String, + pub revision_commit_sha: String, + pub revision_committed_at: DateTime, + pub revision_created_at: DateTime, + pub revision_updated_at: DateTime, + pub revision_deleted_at: Option>, + pub revision_labels: Option, +} + +#[derive(QueryableByName)] +#[diesel(table_name = rfd_pdf_join)] +pub struct RfdPdfJoinRow { pub id: Uuid, pub rfd_number: i32, pub link: Option, @@ -84,8 +137,40 @@ pub struct RfdRevisionModel { pub labels: Option, } -impl From for (RfdModel, RfdRevisionPdfModel) { - fn from(value: RfdPdfsRow) -> Self { +impl From for (RfdModel, RfdRevisionMetaModel) { + fn from(value: RfdMetaJoinRow) -> Self { + ( + RfdModel { + id: value.id, + rfd_number: value.rfd_number, + link: value.link, + created_at: value.created_at, + updated_at: value.updated_at, + deleted_at: value.deleted_at, + visibility: value.visibility, + }, + RfdRevisionMetaModel { + id: value.revision_id, + rfd_id: value.revision_rfd_id, + title: value.revision_title, + state: value.revision_state, + discussion: value.revision_discussion, + authors: value.revision_authors, + content_format: value.revision_content_format, + sha: value.revision_sha, + commit_sha: value.revision_commit_sha, + committed_at: value.revision_committed_at, + created_at: value.revision_created_at, + updated_at: value.revision_updated_at, + deleted_at: value.revision_deleted_at, + labels: value.revision_labels, + }, + ) + } +} + +impl From for (RfdModel, RfdRevisionPdfModel) { + fn from(value: RfdPdfJoinRow) -> Self { ( RfdModel { id: value.id, diff --git a/rfd-model/src/lib.rs b/rfd-model/src/lib.rs index 23b51101..1e4f0638 100644 --- a/rfd-model/src/lib.rs +++ b/rfd-model/src/lib.rs @@ -73,9 +73,9 @@ pub struct Rfd { pub rfd_number: i32, pub link: Option, #[partial(NewRfd(skip))] - #[partial(RfdMeta(retype = RfdRevisionMeta))] - #[partial(RfdPdfs(retype = RfdRevisionPdf))] - pub content: RfdRevision, + #[partial(RfdMeta(retype = Option))] + #[partial(RfdPdfs(retype = Option))] + pub content: Option, #[partial(NewRfd(skip))] pub created_at: DateTime, #[partial(NewRfd(skip))] @@ -85,13 +85,28 @@ pub struct Rfd { pub visibility: Visibility, } +impl From for Rfd { + fn from(value: RfdModel) -> Self { + Self { + id: TypedUuid::from_untyped_uuid(value.id), + rfd_number: value.rfd_number, + link: value.link, + content: None, + created_at: value.created_at, + updated_at: value.updated_at, + deleted_at: value.deleted_at, + visibility: value.visibility, + } + } +} + impl From<(RfdModel, RfdRevisionModel)> for Rfd { fn from((rfd, revision): (RfdModel, RfdRevisionModel)) -> Self { Self { id: TypedUuid::from_untyped_uuid(rfd.id), rfd_number: rfd.rfd_number, link: rfd.link, - content: revision.into(), + content: Some(revision.into()), created_at: rfd.created_at, updated_at: rfd.updated_at, deleted_at: rfd.deleted_at, @@ -106,7 +121,7 @@ impl From<(RfdModel, RfdRevisionMetaModel)> for RfdMeta { id: TypedUuid::from_untyped_uuid(rfd.id), rfd_number: rfd.rfd_number, link: rfd.link, - content: revision.into(), + content: Some(revision.into()), created_at: rfd.created_at, updated_at: rfd.updated_at, deleted_at: rfd.deleted_at, @@ -121,7 +136,7 @@ impl From<(RfdModel, RfdRevisionPdfModel)> for RfdPdfs { id: TypedUuid::from_untyped_uuid(rfd.id), rfd_number: rfd.rfd_number, link: rfd.link, - content: revision.into(), + content: Some(revision.into()), created_at: rfd.created_at, updated_at: rfd.updated_at, deleted_at: rfd.deleted_at, diff --git a/rfd-model/src/schema_ext.rs b/rfd-model/src/schema_ext.rs index 872ee20c..24ec9c06 100644 --- a/rfd-model/src/schema_ext.rs +++ b/rfd-model/src/schema_ext.rs @@ -119,11 +119,70 @@ impl Display for Visibility { } } +// diesel::table! { +// use diesel::sql_types::*; +// use crate::schema::sql_types::{RfdContentFormat, RfdVisibility}; + +// rfd_join (id) { +// id -> Uuid, +// rfd_number -> Integer, +// link -> Nullable, +// created_at -> Timestamptz, +// updated_at -> Timestamptz, +// deleted_at -> Nullable, +// visibility -> RfdVisibility, +// revision_id -> Uuid, +// revision_rfd_id -> Uuid, +// revision_title -> Varchar, +// revision_state -> Nullable, +// revision_discussion -> Nullable, +// revision_authors -> Nullable, +// revision_content -> Varchar, +// revision_content_format -> RfdContentFormat, +// revision_sha -> Varchar, +// revision_commit_sha -> Varchar, +// revision_committed_at -> Timestamptz, +// revision_created_at -> Timestamptz, +// revision_updated_at -> Timestamptz, +// revision_deleted_at -> Nullable, +// revision_labels -> Nullable, +// } +// } + +diesel::table! { + use diesel::sql_types::*; + use crate::schema::sql_types::{ RfdContentFormat, RfdVisibility}; + + rfd_meta_join (id) { + id -> Uuid, + rfd_number -> Integer, + link -> Nullable, + created_at -> Timestamptz, + updated_at -> Timestamptz, + deleted_at -> Nullable, + visibility -> RfdVisibility, + revision_id -> Uuid, + revision_rfd_id -> Uuid, + revision_title -> Varchar, + revision_state -> Nullable, + revision_discussion -> Nullable, + revision_authors -> Nullable, + revision_content_format -> RfdContentFormat, + revision_sha -> Varchar, + revision_commit_sha -> Varchar, + revision_committed_at -> Timestamptz, + revision_created_at -> Timestamptz, + revision_updated_at -> Timestamptz, + revision_deleted_at -> Nullable, + revision_labels -> Nullable, + } +} + diesel::table! { use diesel::sql_types::*; use crate::schema::sql_types::{RfdPdfSource, RfdContentFormat, RfdVisibility}; - rfd_pdfs (id) { + rfd_pdf_join (id) { id -> Uuid, rfd_number -> Integer, link -> Nullable, diff --git a/rfd-model/src/storage/postgres.rs b/rfd-model/src/storage/postgres.rs index eb433133..79234ca0 100644 --- a/rfd-model/src/storage/postgres.rs +++ b/rfd-model/src/storage/postgres.rs @@ -13,7 +13,7 @@ use diesel::{ sql_types::Bool, update, upsert::{excluded, on_constraint}, - BoolExpressionMethods, BoxableExpression, ExpressionMethods, SelectableHelper, + BoolExpressionMethods, BoxableExpression, ExpressionMethods, NullableExpressionMethods, }; use newtype_uuid::{GenericUuid, TypedUuid}; use std::collections::{btree_map::Entry, BTreeMap}; @@ -22,8 +22,8 @@ use v_model::storage::postgres::PostgresStore; use crate::{ db::{ - JobModel, RfdModel, RfdPdfModel, RfdPdfsRow, RfdRevisionMetaModel, RfdRevisionModel, - RfdRevisionPdfModel, + JobModel, RfdMetaJoinRow, RfdModel, RfdPdfJoinRow, RfdPdfModel, RfdRevisionMetaModel, + RfdRevisionModel, RfdRevisionPdfModel, }, schema::{job, rfd, rfd_pdf, rfd_revision}, schema_ext::Visibility, @@ -64,7 +64,7 @@ impl RfdStore for PostgresStore { pagination: &ListPagination, ) -> Result, StoreError> { let mut query = rfd::table - .inner_join(rfd_revision::table) + .left_join(rfd_revision::table) .distinct_on(rfd::id) .into_boxed(); @@ -90,10 +90,11 @@ impl RfdStore for PostgresStore { } if let Some(revision) = revision { - predicates - .push(Box::new(rfd_revision::id.eq_any( - revision.into_iter().map(GenericUuid::into_untyped_uuid), - ))); + predicates.push(Box::new( + rfd_revision::id + .assume_not_null() + .eq_any(revision.into_iter().map(GenericUuid::into_untyped_uuid)), + )); } if let Some(rfd_number) = rfd_number { @@ -102,7 +103,9 @@ impl RfdStore for PostgresStore { if let Some(commit) = commit { predicates.push(Box::new( - rfd_revision::commit_sha.eq_any(commit.into_iter().map(|sha| sha.0)), + rfd_revision::commit_sha + .assume_not_null() + .eq_any(commit.into_iter().map(|sha| sha.0)), )); } @@ -116,7 +119,9 @@ impl RfdStore for PostgresStore { if !deleted { predicates.push(Box::new(rfd::deleted_at.is_null())); - predicates.push(Box::new(rfd_revision::deleted_at.is_null())); + predicates.push(Box::new( + rfd_revision::deleted_at.assume_not_null().is_null(), + )); } predicates @@ -127,19 +132,26 @@ impl RfdStore for PostgresStore { query = query.filter(predicate); } - let results = query + query = query .offset(pagination.offset) .limit(pagination.limit) - .order(( - rfd_revision::rfd_id.asc(), - rfd_revision::committed_at.desc(), - )) - .get_results_async::<(RfdModel, RfdRevisionModel)>(&*self.pool.get().await?) + .order((rfd::id.asc(), rfd_revision::committed_at.desc())); + + tracing::trace!(query = ?debug_query(&query), "List RFDs query"); + + let results = query + .get_results_async::<(RfdModel, Option)>(&*self.pool.get().await?) .await?; tracing::trace!(count = ?results.len(), "Found RFDs"); - Ok(results.into_iter().map(|rfd| rfd.into()).collect()) + Ok(results + .into_iter() + .map(|(rfd, revision)| match revision { + Some(revision) => (rfd, revision).into(), + None => rfd.into(), + }) + .collect()) } async fn upsert(&self, new_rfd: NewRfd) -> Result { @@ -204,86 +216,196 @@ impl RfdMetaStore for PostgresStore { filters: Vec, pagination: &ListPagination, ) -> Result, StoreError> { - let mut query = rfd::table - .inner_join(rfd_revision::table) - .distinct_on(rfd::id) - .select((RfdModel::as_select(), RfdRevisionMetaModel::as_select())) - .into_boxed(); - tracing::trace!(?filters, "Lookup RFDs"); - let filter_predicates = filters - .into_iter() - .map(|filter| { - let mut predicates: Vec>> = vec![]; - let RfdFilter { - id, - revision, - rfd_number, - commit, - public, - deleted, - } = filter; + let mut clauses = vec![]; + let mut bind_count = 1; - if let Some(id) = id { - predicates.push(Box::new( - rfd::id.eq_any(id.into_iter().map(GenericUuid::into_untyped_uuid)), - )); - } + for filter in &filters { + let mut filter_clause = "1=1".to_string(); - if let Some(revision) = revision { - predicates - .push(Box::new(rfd_revision::id.eq_any( - revision.into_iter().map(GenericUuid::into_untyped_uuid), - ))); - } + let RfdFilter { + id, + revision, + rfd_number, + commit, + public, + deleted, + } = filter; - if let Some(rfd_number) = rfd_number { - predicates.push(Box::new(rfd::rfd_number.eq_any(rfd_number))); - } + if let Some(ids) = &id { + let id_binds = ids + .iter() + .enumerate() + .map(|(i, _)| format!("${}", bind_count + i)) + .collect::>(); + bind_count = bind_count + id_binds.len(); + filter_clause = filter_clause + &format!(" AND rfd.id IN ({})", id_binds.join(",")); + } - if let Some(commit) = commit { - predicates.push(Box::new( - rfd_revision::commit_sha.eq_any(commit.into_iter().map(|sha| sha.0)), - )); + if let Some(revisions) = &revision { + let revision_binds = revisions + .iter() + .enumerate() + .map(|(i, _)| format!("${}", bind_count + i)) + .collect::>(); + bind_count = bind_count + revision_binds.len(); + filter_clause = filter_clause + + &format!(" AND rfd_revision.id IN ({})", revision_binds.join(",")); + } + + if let Some(rfd_numbers) = &rfd_number { + let rfd_number_binds = rfd_numbers + .iter() + .enumerate() + .map(|(i, _)| format!("${}", bind_count + i)) + .collect::>(); + bind_count = bind_count + rfd_number_binds.len(); + filter_clause = filter_clause + + &format!(" AND rfd.rfd_number IN ({})", rfd_number_binds.join(",")); + } + + if let Some(commit_shas) = &commit { + let commit_sha_binds = commit_shas + .iter() + .enumerate() + .map(|(i, _)| format!("${}", bind_count + i)) + .collect::>(); + bind_count = bind_count + commit_sha_binds.len(); + filter_clause = filter_clause + + &format!( + " AND rfd_revision.commit_sha IN ({})", + commit_sha_binds.join(",") + ); + } + + if let Some(_) = &public { + bind_count = bind_count + 1; + filter_clause = filter_clause + " AND rfd.public = {}"; + } + + if !deleted { + filter_clause = filter_clause + + " AND rfd.deleted_at IS NULL AND rfd_revision.deleted_at IS NULL"; + } + + clauses.push(format!("({})", filter_clause)); + } + + let where_clause = if clauses.len() > 0 { + format!("({})", clauses.join(" OR ")) + } else { + format!("1=1") + }; + + let raw_query = format!( + r#"SELECT + rfd.id as id, + rfd.rfd_number as rfd_number, + rfd.link as link, + rfd.created_at as created_at, + rfd.updated_at as updated_at, + rfd.deleted_at as deleted_at, + rfd.visibility as visibility, + rfd_revision.id AS revision_id, + rfd_revision.rfd_id as revision_rfd_id, + rfd_revision.title as revision_title, + rfd_revision.state as revision_state, + rfd_revision.discussion as revision_discussion, + rfd_revision.authors as revision_authors, + rfd_revision.content_format as revision_content_format, + rfd_revision.sha as revision_sha, + rfd_revision.commit_sha as revision_commit_sha, + rfd_revision.committed_at as revision_committed_at, + rfd_revision.created_at as revision_created_at, + rfd_revision.updated_at as revision_updated_at, + rfd_revision.deleted_at as revision_deleted_at, + rfd_revision.labels as revision_labels + FROM + rfd + INNER JOIN + rfd_revision + ON rfd_revision.rfd_id = rfd.id + WHERE {} AND + rfd_revision.id = ( + SELECT rfd_revision.id + FROM rfd_revision + WHERE rfd_revision.rfd_id = rfd.id + ORDER BY rfd_revision.committed_at DESC + LIMIT 1 + ) + ORDER BY + rfd_revision.rfd_id ASC, + rfd_revision.committed_at DESC + LIMIT ${} OFFSET ${}"#, + where_clause, + bind_count, + bind_count + 1, + ); + + let mut query = sql_query(raw_query).into_boxed::(); + + for filter in &filters { + let RfdFilter { + id, + revision, + rfd_number, + commit, + public, + .. + } = filter; + + if let Some(ids) = &id { + for id in ids { + tracing::trace!(?id, "Binding id parameter"); + query = query.bind::(id.into_untyped_uuid()); } + } - if let Some(public) = public { - predicates.push(Box::new( - rfd::visibility.eq(public - .then(|| Visibility::Public) - .unwrap_or(Visibility::Private)), - )); + if let Some(revisions) = &revision { + for revision in revisions { + tracing::trace!(?revision, "Binding revision parameter"); + query = query.bind::(revision.into_untyped_uuid()); } + } - if !deleted { - predicates.push(Box::new(rfd::deleted_at.is_null())); - predicates.push(Box::new(rfd_revision::deleted_at.is_null())); + if let Some(rfd_numbers) = &rfd_number { + for rfd_number in rfd_numbers { + tracing::trace!(?rfd_number, "Binding rfd_number parameter"); + query = query.bind::(*rfd_number); } + } - predicates - }) - .collect::>(); + if let Some(commits) = &commit { + for commit in commits { + tracing::trace!(?commit, "Binding commit parameter"); + query = query.bind::(commit.to_string()); + } + } - if let Some(predicate) = flatten_predicates(filter_predicates) { - query = query.filter(predicate); + if let Some(public) = &public { + tracing::trace!(?public, "Binding public parameter"); + query = query.bind::(*public); + } } query = query - .offset(pagination.offset) - .limit(pagination.limit) - .order(( - rfd_revision::rfd_id.asc(), - rfd_revision::committed_at.desc(), - )); + .bind::(pagination.limit as i32) + .bind::(pagination.offset as i32); - let results = query - .get_results_async::<(RfdModel, RfdRevisionMetaModel)>(&*self.pool.get().await?) + tracing::trace!(query = ?debug_query(&query), "List RFDs query"); + + let rows = query + .get_results_async::(&*self.pool.get().await?) .await?; + let results = rows + .into_iter() + .map(|row| <(RfdModel, RfdRevisionMetaModel)>::from(row).into()) + .collect::>(); tracing::trace!(count = ?results.len(), "Found RFDs"); - Ok(results.into_iter().map(|rfd| rfd.into()).collect()) + Ok(results) } } @@ -358,7 +480,7 @@ impl RfdPdfsStore for PostgresStore { .collect::>(); bind_count = bind_count + rfd_number_binds.len(); filter_clause = filter_clause - + &format!(" AND rfd.rfd_number.id IN ({})", rfd_number_binds.join(",")); + + &format!(" AND rfd.rfd_number IN ({})", rfd_number_binds.join(",")); } if let Some(commit_shas) = &commit { @@ -370,7 +492,7 @@ impl RfdPdfsStore for PostgresStore { bind_count = bind_count + commit_sha_binds.len(); filter_clause = filter_clause + &format!( - " AND rfd_revision.commit_sha.id IN ({})", + " AND rfd_revision.commit_sha IN ({})", commit_sha_binds.join(",") ); } @@ -384,43 +506,47 @@ impl RfdPdfsStore for PostgresStore { filter_clause = filter_clause +" AND rfd.deleted_at IS NULL AND rfd_revision.deleted_at IS NULL AND rfd_pdf.deleted_at IS NULL"; } - clauses.push(filter_clause); + clauses.push(format!("({})", filter_clause)); } - let where_clause = clauses.join(" OR "); + let where_clause = if clauses.len() > 0 { + format!("({})", clauses.join(" OR ")) + } else { + format!("1=1") + }; let raw_query = format!( r#"SELECT - rfd.id, - rfd.rfd_number, - rfd.link, - rfd.created_at, - rfd.updated_at, - rfd.deleted_at, - rfd.visibility, + rfd.id as id, + rfd.rfd_number as rfd_number, + rfd.link as link, + rfd.created_at as created_at, + rfd.updated_at as updated_at, + rfd.deleted_at as deleted_at, + rfd.visibility as visibility, rfd_revision.id AS revision_id, - rfd_revision.rfd_id, - rfd_revision.title, - rfd_revision.state, - rfd_revision.discussion, - rfd_revision.authors, - rfd_pdf.id, - rfd_pdf.rfd_revision_id, - rfd_pdf.source, - rfd_pdf.link, - rfd_pdf.created_at, - rfd_pdf.updated_at, - rfd_pdf.deleted_at, - rfd_pdf.rfd_id, - rfd_pdf.external_id, - rfd_revision.content_format, - rfd_revision.sha, - rfd_revision.commit_sha, - rfd_revision.committed_at, - rfd_revision.created_at, - rfd_revision.updated_at, - rfd_revision.deleted_at, - rfd_revision.labels + rfd_revision.rfd_id as revision_rfd_id, + rfd_revision.title as revision_title, + rfd_revision.state as revision_state, + rfd_revision.discussion as revision_discussion, + rfd_revision.authors as revision_authors, + rfd_pdf.id as pdf_id, + rfd_pdf.rfd_revision_id as pdf_rfd_revision_id, + rfd_pdf.source as pfd_source, + rfd_pdf.link as pdf_link, + rfd_pdf.created_at as pdf_created_at, + rfd_pdf.updated_at as pdf_updated_at, + rfd_pdf.deleted_at as pdf_deleted_at, + rfd_pdf.rfd_id as pdf_rfd_id, + rfd_pdf.external_id as pdf_external_id, + rfd_revision.content_format as revision_content_format, + rfd_revision.sha as revision_sha, + rfd_revision.commit_sha as revision_commit_sha, + rfd_revision.committed_at as revision_committed_at, + rfd_revision.created_at as revision_created_at, + rfd_revision.updated_at as revision_updated_at, + rfd_revision.deleted_at as revision_deleted_at, + rfd_revision.labels as revision_labels FROM rfd INNER JOIN @@ -496,8 +622,10 @@ impl RfdPdfsStore for PostgresStore { .bind::(pagination.limit as i32) .bind::(pagination.offset as i32); + tracing::trace!(query = ?debug_query(&query), "List RFDs query"); + let rows = query - .get_results_async::(&*self.pool.get().await?) + .get_results_async::(&*self.pool.get().await?) .await?; let results = rows .into_iter() @@ -511,6 +639,8 @@ impl RfdPdfsStore for PostgresStore { existing .get_mut() .content + .as_mut() + .unwrap() .content .push(revision_model.content.into()); }