Skip to content

Commit

Permalink
feat(cat-gateway): Catalyst signed docs endpoints initial implementat…
Browse files Browse the repository at this point in the history
…ion (#1510)

* feat(cat-gateway): Add document get endpoint

* feat(cat-gateway): Add generic cbor payload type

* fix(docs): spelling

* fix(cat-gateway): code format

* feat(cat-gateway): Add document put endpoint

* fix(cat-gateway): fix documentation for the error responses.

* refactor RetryAfterOption

* fix documentation for the error responses.

* update full signed doc query

* add put documents endpoint initial impl

* update put_document impl and get_document impl

* fix CI checks

* updating author field to be an array of TEXT

* feat(general): Bump cat-ci to v3.2.31 to get documentation updates

* feat(docs): Add alternative OpenAPI viewers because Elements has issues

* add authors

* wip

* fix error handling

* fix

* fix

* fix errors

* update catalyst-signed-doc crate ver

* fix

* revert

* fix

* fix sqlfluff

---------

Co-authored-by: Steven Johnson <[email protected]>
Co-authored-by: Steven Johnson <[email protected]>
  • Loading branch information
3 people authored Jan 17, 2025
1 parent 2f3bbbe commit f83a33f
Show file tree
Hide file tree
Showing 17 changed files with 156 additions and 72 deletions.
2 changes: 2 additions & 0 deletions catalyst-gateway/bin/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ workspace = true
cardano-chain-follower = {version = "0.0.6", git = "https://github.com/input-output-hk/catalyst-libs.git", tag="v0.0.10" }
c509-certificate = { version = "0.0.3", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.3" }
rbac-registration = { version = "0.0.2", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "v0.0.8" }
catalyst-signed-doc = { version = "0.0.1", git = "https://github.com/input-output-hk/catalyst-libs.git", tag = "r20250116-00" }


pallas = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
pallas-traverse = { version = "0.30.1", git = "https://github.com/input-output-hk/catalyst-pallas.git", rev = "9b5183c8b90b90fe2cc319d986e933e9518957b3" }
Expand Down
28 changes: 18 additions & 10 deletions catalyst-gateway/bin/src/db/event/signed_docs/full_signed_doc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ impl FullSignedDoc {

/// Returns the document author.
#[allow(dead_code)]
pub(crate) fn author(&self) -> &String {
self.body.author()
pub(crate) fn authors(&self) -> &Vec<String> {
self.body.authors()
}

/// Returns the `SignedDocBody`.
Expand All @@ -57,7 +57,15 @@ impl FullSignedDoc {
&self.body
}

/// Returns the document raw bytes.
#[allow(dead_code)]
pub(crate) fn raw(&self) -> &Vec<u8> {
&self.raw
}

/// Uploads a `FullSignedDoc` to the event db.
/// Returns `true` if document was added into the db, `false` if it was already added
/// previously.
///
/// Make an insert query into the `event-db` by adding data into the `signed_docs`
/// table.
Expand All @@ -73,21 +81,21 @@ impl FullSignedDoc {
/// - `ver` is a UUID v7
/// - `doc_type` is a UUID v4
#[allow(dead_code)]
pub(crate) async fn store(&self) -> anyhow::Result<()> {
pub(crate) async fn store(&self) -> anyhow::Result<bool> {
match Self::retrieve(self.id(), Some(self.ver())).await {
Ok(res_doc) => {
anyhow::ensure!(
&res_doc == self,
"Document with the same `id` and `ver` already exists"
);
return Ok(());
Ok(false)
},
Err(err) if err.is::<NotFoundError>() => {},
Err(err) => return Err(err),
Err(err) if err.is::<NotFoundError>() => {
EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?;
Ok(true)
},
Err(err) => Err(err),
}

EventDB::modify(INSERT_SIGNED_DOCS, &self.postgres_db_fields()).await?;
Ok(())
}

/// Loads a `FullSignedDoc` from the event db.
Expand Down Expand Up @@ -146,7 +154,7 @@ impl FullSignedDoc {
*id,
ver,
row.try_get("type")?,
row.try_get("author")?,
row.try_get("authors")?,
row.try_get("metadata")?,
),
payload: row.try_get("payload")?,
Expand Down
4 changes: 2 additions & 2 deletions catalyst-gateway/bin/src/db/event/signed_docs/query_filter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ pub(crate) enum DocsQueryFilter {
DocId(uuid::Uuid),
/// Select docs with the specific `id` and `ver` field
DocVer(uuid::Uuid, uuid::Uuid),
/// Select docs with the specific `author` field
/// Select docs with the specific `authors` field
Author(String),
}

Expand All @@ -26,7 +26,7 @@ impl Display for DocsQueryFilter {
Self::DocVer(id, ver) => {
write!(f, "signed_docs.id = '{id}' AND signed_docs.ver = '{ver}'")
},
Self::Author(author) => write!(f, "signed_docs.author = '{author}'"),
Self::Author(author) => write!(f, "signed_docs.authors @> '{{ \"{author}\" }}'"),
}
}
}
21 changes: 10 additions & 11 deletions catalyst-gateway/bin/src/db/event/signed_docs/signed_doc_body.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ pub(crate) struct SignedDocBody {
ver: uuid::Uuid,
/// `signed_doc` table `type` field
doc_type: uuid::Uuid,
/// `signed_doc` table `author` field
author: String,
/// `signed_doc` table `authors` field
authors: Vec<String>,
/// `signed_doc` table `metadata` field
metadata: Option<serde_json::Value>,
}
Expand All @@ -40,9 +40,9 @@ impl SignedDocBody {
&self.ver
}

/// Returns the document author.
pub(crate) fn author(&self) -> &String {
&self.author
/// Returns the document authors.
pub(crate) fn authors(&self) -> &Vec<String> {
&self.authors
}

/// Returns all signed document fields for the event db queries
Expand All @@ -51,22 +51,21 @@ impl SignedDocBody {
&self.id,
&self.ver,
&self.doc_type,
&self.author,
&self.authors,
&self.metadata,
]
}

/// Creates a `SignedDocBody` instance.
#[allow(dead_code)]
pub(crate) fn new(
id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, author: String,
id: uuid::Uuid, ver: uuid::Uuid, doc_type: uuid::Uuid, authors: Vec<String>,
metadata: Option<serde_json::Value>,
) -> Self {
Self {
id,
ver,
doc_type,
author,
authors,
metadata,
}
}
Expand All @@ -91,13 +90,13 @@ impl SignedDocBody {
let id = row.try_get("id")?;
let ver = row.try_get("ver")?;
let doc_type = row.try_get("type")?;
let author = row.try_get("author")?;
let authors = row.try_get("authors")?;
let metadata = row.try_get("metadata")?;
Ok(Self {
id,
ver,
doc_type,
author,
authors,
metadata,
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ SELECT
signed_docs.id,
signed_docs.ver,
signed_docs.type,
signed_docs.author,
signed_docs.authors,
signed_docs.metadata
FROM signed_docs
WHERE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ INSERT INTO signed_docs
id,
ver,
type,
author,
authors,
metadata,
payload,
raw
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
SELECT
{% if not ver %} signed_docs.ver, {% endif %}
signed_docs.type,
signed_docs.author,
signed_docs.authors,
signed_docs.metadata,
signed_docs.payload,
signed_docs.raw
Expand Down
20 changes: 13 additions & 7 deletions catalyst-gateway/bin/src/db/event/signed_docs/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ async fn queries_test() {
uuid::Uuid::now_v7(),
uuid::Uuid::now_v7(),
doc_type,
"Alex".to_string(),
vec!["Alex".to_string()],
Some(serde_json::Value::Null),
),
Some(serde_json::Value::Null),
Expand All @@ -29,7 +29,7 @@ async fn queries_test() {
uuid::Uuid::now_v7(),
uuid::Uuid::now_v7(),
doc_type,
"Steven".to_string(),
vec!["Steven".to_string()],
Some(serde_json::Value::Null),
),
Some(serde_json::Value::Null),
Expand All @@ -40,7 +40,7 @@ async fn queries_test() {
uuid::Uuid::now_v7(),
uuid::Uuid::now_v7(),
doc_type,
"Sasha".to_string(),
vec!["Sasha".to_string()],
None,
),
None,
Expand All @@ -49,12 +49,18 @@ async fn queries_test() {
];

for doc in &docs {
doc.store().await.unwrap();
assert!(doc.store().await.unwrap());
// try to insert the same data again
doc.store().await.unwrap();
assert!(!doc.store().await.unwrap());
// try another doc with the same `id` and `ver` and with different other fields
let another_doc = FullSignedDoc::new(
SignedDocBody::new(*doc.id(), *doc.ver(), doc_type, "Neil".to_string(), None),
SignedDocBody::new(
*doc.id(),
*doc.ver(),
doc_type,
vec!["Neil".to_string()],
None,
),
None,
vec![],
);
Expand Down Expand Up @@ -87,7 +93,7 @@ async fn queries_test() {
assert!(res_docs.try_next().await.unwrap().is_none());

let mut res_docs = SignedDocBody::retrieve(
&DocsQueryFilter::Author(doc.author().clone()),
&DocsQueryFilter::Author(doc.authors().first().unwrap().clone()),
&QueryLimits::ALL,
)
.await
Expand Down
17 changes: 10 additions & 7 deletions catalyst-gateway/bin/src/service/api/documents/get_document.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
//! Implementation of the GET `/document` endpoint
use poem::Body;
use poem_openapi::ApiResponse;

use crate::service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor};
use crate::{
db::event::{error::NotFoundError, signed_docs::FullSignedDoc},
service::common::{responses::WithErrorResponses, types::payload::cbor::Cbor},
};

/// Endpoint responses.
#[derive(ApiResponse)]
Expand All @@ -13,7 +15,7 @@ pub(crate) enum Responses {
///
/// The Document that was requested.
#[oai(status = 200)]
Ok(Cbor<Body>),
Ok(Cbor<Vec<u8>>),
/// ## Not Found
///
/// The document could not be found.
Expand All @@ -27,8 +29,9 @@ pub(crate) type AllResponses = WithErrorResponses<Responses>;
/// # GET `/document`
#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)]
pub(crate) async fn endpoint(document_id: uuid::Uuid, version: Option<uuid::Uuid>) -> AllResponses {
let _doc = document_id;
let _ver = version;

Responses::NotFound.into()
match FullSignedDoc::retrieve(&document_id, version.as_ref()).await {
Ok(doc) => Responses::Ok(Cbor(doc.raw().clone())).into(),
Err(err) if err.is::<NotFoundError>() => Responses::NotFound.into(),
Err(err) => AllResponses::handle_error(&err),
}
}
20 changes: 12 additions & 8 deletions catalyst-gateway/bin/src/service/api/documents/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ use put_document::{bad_put_request::PutDocumentBadRequest, MAXIMUM_DOCUMENT_SIZE

use crate::service::{
common::{
self,
auth::{none_or_rbac::NoneOrRBAC, rbac::scheme::CatalystRBACSecurityScheme},
auth::rbac::scheme::CatalystRBACSecurityScheme,
tags::ApiTags,
types::{generic::uuidv7::UUIDv7, payload::cbor::Cbor},
types::{
generic::{
query::pagination::{Limit, Page},
uuidv7::UUIDv7,
},
payload::cbor::Cbor,
},
},
utilities::middleware::schema_validation::schema_version_validation,
};
Expand Down Expand Up @@ -46,7 +51,7 @@ impl DocumentApi {
/// version.
version: Query<Option<UUIDv7>>,
/// No Authorization required, but Token permitted.
_auth: NoneOrRBAC,
_auth: CatalystRBACSecurityScheme,
) -> get_document::AllResponses {
let Ok(doc_id) = document_id.0.try_into() else {
let err = anyhow!("Invalid UUIDv7"); // Should not happen as UUIDv7 is validating.
Expand Down Expand Up @@ -76,9 +81,9 @@ impl DocumentApi {
_auth: CatalystRBACSecurityScheme,
) -> put_document::AllResponses {
match document.0.into_bytes_limit(MAXIMUM_DOCUMENT_SIZE).await {
Ok(document) => put_document::endpoint(document).await,
Ok(doc_bytes) => put_document::endpoint(doc_bytes.to_vec()).await,
Err(ReadBodyError::PayloadTooLarge) => put_document::Responses::PayloadTooLarge.into(),
Err(_err) => {
Err(_) => {
put_document::Responses::BadRequest(Json(PutDocumentBadRequest::new(
"Failed to read document from the request",
)))
Expand All @@ -103,8 +108,7 @@ impl DocumentApi {
async fn post_document(
&self, /// The Query Filter Specification
query: Json<DocumentIndexQueryFilterBody>,
page: Query<Option<common::types::generic::query::pagination::Page>>,
limit: Query<Option<common::types::generic::query::pagination::Limit>>,
page: Query<Option<Page>>, limit: Query<Option<Limit>>,
/// Authorization required.
_auth: CatalystRBACSecurityScheme,
) -> post_document_index_query::AllResponses {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use poem_openapi::{payload::Json, ApiResponse, Object};
use query_filter::DocumentIndexQueryFilter;
use response::DocumentIndexListDocumented;

use super::common;
use super::{Limit, Page};
use crate::service::common::responses::WithErrorResponses;

pub(crate) mod query_filter;
Expand Down Expand Up @@ -39,9 +39,7 @@ pub(crate) struct QueryDocumentIndex {
/// # POST `/document/index`
#[allow(clippy::unused_async, clippy::no_effect_underscore_binding)]
pub(crate) async fn endpoint(
filter: DocumentIndexQueryFilter,
page: Option<common::types::generic::query::pagination::Page>,
limit: Option<common::types::generic::query::pagination::Limit>,
filter: DocumentIndexQueryFilter, page: Option<Page>, limit: Option<Limit>,
) -> AllResponses {
let _filter = filter;
let _page = page;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@

use poem_openapi::{types::Example, NewType, Object};

use super::common::types::document::ver::EqOrRangedVerDocumented;
use crate::service::common::types::document::{
doc_ref::IdAndVerRefDocumented, doc_type::DocumentType, id::EqOrRangedIdDocumented,
ver::EqOrRangedVerDocumented,
};

/// Query Filter for the generation of a signed document index.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,9 @@ pub(crate) struct PutDocumentBadRequest {

impl PutDocumentBadRequest {
/// Create a new instance of `ConfigBadRequest`.
pub(crate) fn new(error: &str) -> Self {
pub(crate) fn new(error: &(impl ToString + ?Sized)) -> Self {
Self {
error: error.to_owned(),
error: error.to_string(),
}
}
}
Expand Down
Loading

0 comments on commit f83a33f

Please sign in to comment.