Skip to content

Commit 87720c6

Browse files
authored
server: Add support for creating apps in CMG (#2095)
Add support for creating app in CMG part of: svix/monorepo-private#11626
2 parents 16d1fe1 + 7d98f68 commit 87720c6

File tree

7 files changed

+257
-21
lines changed

7 files changed

+257
-21
lines changed

server/openapi.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1931,6 +1931,11 @@
19311931
},
19321932
"MessageIn": {
19331933
"properties": {
1934+
"application": {
1935+
"$ref": "#/components/schemas/ApplicationIn",
1936+
"description": "Optionally creates a new application alongside the message.\n\nIf the application id or uid that is used in the path already exists, this argument is ignored.",
1937+
"nullable": true
1938+
},
19341939
"channels": {
19351940
"description": "List of free-form identifiers that endpoints can filter by",
19361941
"example": [
@@ -5291,6 +5296,19 @@
52915296
},
52925297
"style": "form"
52935298
},
5299+
{
5300+
"in": "path",
5301+
"name": "app_id",
5302+
"required": true,
5303+
"schema": {
5304+
"example": "unique-app-identifier",
5305+
"maxLength": 256,
5306+
"minLength": 1,
5307+
"pattern": "^[a-zA-Z0-9\\-_.]+$",
5308+
"type": "string"
5309+
},
5310+
"style": "simple"
5311+
},
52945312
{
52955313
"description": "The request's idempotency key",
52965314
"in": "header",

server/svix-server/src/v1/endpoints/application.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,15 @@ use axum::{
1212
use chrono::{DateTime, Utc};
1313
use futures::FutureExt;
1414
use schemars::JsonSchema;
15-
use sea_orm::{ActiveModelTrait, ActiveValue::Set, TransactionTrait};
15+
use sea_orm::{ActiveModelTrait, ActiveValue::Set, DatabaseConnection, TransactionTrait};
1616
use serde::{Deserialize, Serialize};
1717
use svix_server_derive::{aide_annotate, ModelOut};
1818
use validator::{Validate, ValidationError};
1919

2020
use crate::{
2121
core::{
2222
permissions,
23-
types::{metadata::Metadata, ApplicationId, ApplicationUid},
23+
types::{metadata::Metadata, ApplicationId, ApplicationUid, OrganizationId},
2424
},
2525
db::models::{application, applicationmetadata},
2626
error::{http_error_on_conflict, HttpError, Result, Traceable},
@@ -270,11 +270,21 @@ async fn create_application(
270270
};
271271
}
272272

273-
let app = application::ActiveModel::new(org_id.clone());
273+
let (app, metadata) = create_app_from_app_in(db, data, org_id).await?;
274+
275+
Ok(JsonStatusUpsert::Created((app, metadata).into()))
276+
}
277+
278+
pub async fn create_app_from_app_in(
279+
db: &DatabaseConnection,
280+
app_in: ApplicationIn,
281+
org_id: OrganizationId,
282+
) -> Result<(application::Model, applicationmetadata::Model)> {
283+
let app = application::ActiveModel::new(org_id);
274284
let metadata = applicationmetadata::ActiveModel::new(app.id.clone().unwrap(), None);
275285

276286
let mut model = (app, metadata);
277-
data.update_model(&mut model);
287+
app_in.update_model(&mut model);
278288
let (app, metadata) = model;
279289

280290
let (app, metadata) = db
@@ -288,7 +298,7 @@ async fn create_application(
288298
})
289299
.await?;
290300

291-
Ok(JsonStatusUpsert::Created((app, metadata).into()))
301+
Ok((app, metadata))
292302
}
293303

294304
/// Get an application.

server/svix-server/src/v1/endpoints/endpoint/mod.rs

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ use crate::{
3131
cryptography::Encryption,
3232
permissions,
3333
types::{
34-
metadata::Metadata, EndpointHeaders, EndpointHeadersPatch, EndpointId, EndpointSecret,
35-
EndpointSecretInternal, EndpointUid, EventChannelSet, EventTypeName, EventTypeNameSet,
36-
MessageStatus,
34+
metadata::Metadata, ApplicationIdOrUid, EndpointHeaders, EndpointHeadersPatch,
35+
EndpointId, EndpointSecret, EndpointSecretInternal, EndpointUid, EventChannelSet,
36+
EventTypeName, EventTypeNameSet, MessageStatus,
3737
},
3838
},
3939
db::models::{
@@ -802,10 +802,20 @@ async fn send_example(
802802
uid: None,
803803
payload_retention_period: 90,
804804
extra_params: None,
805+
application: None,
805806
};
806807

807-
let create_message =
808-
create_message_inner(db, queue_tx, cache, false, Some(endpoint.id), msg_in, app).await?;
808+
let create_message = create_message_inner(
809+
db,
810+
queue_tx,
811+
cache,
812+
false,
813+
Some(endpoint.id),
814+
msg_in,
815+
app.org_id,
816+
ApplicationIdOrUid(app.id.0),
817+
)
818+
.await?;
809819

810820
Ok(Json(create_message))
811821
}

server/svix-server/src/v1/endpoints/message.rs

Lines changed: 94 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,21 @@ use crate::{
2525
message_app::CreateMessageApp,
2626
permissions,
2727
types::{
28-
EndpointId, EventChannel, EventChannelSet, EventTypeName, EventTypeNameSet,
29-
MessageAttemptTriggerType, MessageId, MessageUid,
28+
ApplicationIdOrUid, EndpointId, EventChannel, EventChannelSet, EventTypeName,
29+
EventTypeNameSet, MessageAttemptTriggerType, MessageId, MessageUid, OrganizationId,
3030
},
3131
},
3232
db::models::{application, message, messagecontent},
33-
error::{http_error_on_conflict, Error, HttpError, Result},
33+
error::{http_error_on_conflict, Error, HttpError, Result, ValidationErrorItem},
3434
queue::{MessageTaskBatch, TaskQueueProducer},
35-
v1::utils::{
36-
filter_and_paginate_time_limited, openapi_tag, validation_error, ApplicationMsgPath,
37-
EventTypesQueryParams, JsonStatus, ListResponse, ModelIn, ModelOut, PaginationDescending,
38-
PaginationLimit, ReversibleIterator, ValidatedJson, ValidatedQuery,
35+
v1::{
36+
endpoints::application::{create_app_from_app_in, ApplicationIn},
37+
utils::{
38+
filter_and_paginate_time_limited, openapi_tag, validation_error, validation_errors,
39+
ApplicationMsgPath, ApplicationPath, EventTypesQueryParams, JsonStatus, ListResponse,
40+
ModelIn, ModelOut, PaginationDescending, PaginationLimit, ReversibleIterator,
41+
ValidatedJson, ValidatedQuery,
42+
},
3943
},
4044
AppState,
4145
};
@@ -127,6 +131,14 @@ pub struct MessageIn {
127131
#[serde(rename = "transformationsParams")]
128132
#[schemars(skip)]
129133
pub extra_params: Option<MessageInExtraParams>,
134+
135+
/// Optionally creates a new application alongside the message.
136+
///
137+
/// If the application id or uid that is used in the path already exists,
138+
/// this argument is ignored.
139+
#[validate]
140+
#[serde(skip_serializing_if = "Option::is_none")]
141+
pub application: Option<ApplicationIn>,
130142
}
131143

132144
impl MessageIn {
@@ -335,23 +347,64 @@ async fn create_message(
335347
ValidatedQuery(CreateMessageQueryParams { with_content }): ValidatedQuery<
336348
CreateMessageQueryParams,
337349
>,
338-
permissions::OrganizationWithApplication { app }: permissions::OrganizationWithApplication,
350+
Path(ApplicationPath { app_id }): Path<ApplicationPath>,
351+
permissions::Organization { org_id }: permissions::Organization,
339352
ValidatedJson(data): ValidatedJson<MessageIn>,
340353
) -> Result<JsonStatus<202, MessageOut>> {
341354
Ok(JsonStatus(
342-
create_message_inner(db, queue_tx, cache, with_content, None, data, app).await?,
355+
create_message_inner(
356+
db,
357+
queue_tx,
358+
cache,
359+
with_content,
360+
None,
361+
data,
362+
org_id,
363+
app_id,
364+
)
365+
.await?,
343366
))
344367
}
345368

369+
#[allow(clippy::too_many_arguments)]
346370
pub(crate) async fn create_message_inner(
347371
db: &DatabaseConnection,
348372
queue_tx: TaskQueueProducer,
349373
cache: Cache,
350374
with_content: bool,
351375
force_endpoint: Option<EndpointId>,
352376
data: MessageIn,
353-
app: application::Model,
377+
org_id: OrganizationId,
378+
app_id: ApplicationIdOrUid,
354379
) -> Result<MessageOut> {
380+
app_id.validate().map_err(|e| {
381+
HttpError::unprocessable_entity(validation_errors(
382+
vec!["path".to_owned(), "app_id_or_uid".to_owned()],
383+
e,
384+
))
385+
})?;
386+
387+
let app_from_path_app_id =
388+
application::Entity::secure_find_by_id_or_uid(org_id.clone(), app_id.to_owned())
389+
.one(db)
390+
.await?;
391+
392+
let app = match (&data.application, app_from_path_app_id) {
393+
(None, None) => {
394+
return Err(
395+
HttpError::not_found(None, Some("Application not found".to_string())).into(),
396+
);
397+
}
398+
399+
(_, Some(app_from_path_param)) => app_from_path_param,
400+
(Some(cmg_app), None) => {
401+
validate_create_app_uid(&app_id, cmg_app)?;
402+
let (app, _metadata) = create_app_from_app_in(db, cmg_app.to_owned(), org_id).await?;
403+
404+
app
405+
}
406+
};
407+
355408
let create_message_app = CreateMessageApp::layered_fetch(
356409
&cache,
357410
db,
@@ -407,6 +460,37 @@ pub(crate) async fn create_message_inner(
407460
Ok(msg_out)
408461
}
409462

463+
fn validate_create_app_uid(app_id_or_uid: &ApplicationIdOrUid, data: &ApplicationIn) -> Result<()> {
464+
// If implicit app creation is requested then the UID must be set
465+
// in the request body, and it must match the UID given in the path
466+
if let Some(uid) = &data.uid {
467+
if uid.0 != app_id_or_uid.0 {
468+
return Err(HttpError::unprocessable_entity(vec![ValidationErrorItem {
469+
loc: vec![
470+
"body".to_string(),
471+
"application".to_string(),
472+
"uid".to_string(),
473+
],
474+
msg: "Application UID in the path and body must match".to_string(),
475+
ty: "application_uid_mismatch".to_string(),
476+
}])
477+
.into());
478+
}
479+
} else {
480+
return Err(HttpError::unprocessable_entity(vec![ValidationErrorItem {
481+
loc: vec![
482+
"body".to_string(),
483+
"application".to_string(),
484+
"uid".to_string(),
485+
],
486+
msg: "Application UID not set in body".to_string(),
487+
ty: "application_uid_missing".to_string(),
488+
}])
489+
.into());
490+
}
491+
Ok(())
492+
}
493+
410494
#[derive(Debug, Deserialize, Validate, JsonSchema)]
411495
pub struct GetMessageQueryParams {
412496
/// When `true` message payloads are included in the response

server/svix-server/src/v1/utils/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -500,7 +500,7 @@ pub fn validation_error(code: Option<&'static str>, msg: Option<&'static str>) -
500500

501501
/// Recursively searches a [`validator::ValidationErrors`] tree into a linear list of errors to be
502502
/// sent to the user
503-
fn validation_errors(
503+
pub fn validation_errors(
504504
acc_path: Vec<String>,
505505
err: validator::ValidationErrors,
506506
) -> Vec<ValidationErrorItem> {

0 commit comments

Comments
 (0)