feat: add workspace foundation
This commit is contained in:
@@ -6,6 +6,10 @@ pub enum RegistryError {
|
||||
Storage(#[from] sqlx::Error),
|
||||
#[error(transparent)]
|
||||
Serialization(#[from] serde_json::Error),
|
||||
#[error("workspace {workspace_id} was not found")]
|
||||
WorkspaceNotFound { workspace_id: String },
|
||||
#[error("workspace with slug {slug} already exists")]
|
||||
WorkspaceSlugAlreadyExists { slug: String },
|
||||
#[error("operation {operation_id} already exists")]
|
||||
OperationAlreadyExists { operation_id: String },
|
||||
#[error("operation {operation_id} was not found")]
|
||||
|
||||
@@ -5,10 +5,10 @@ mod postgres;
|
||||
|
||||
pub use error::RegistryError;
|
||||
pub use model::{
|
||||
CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishRequest,
|
||||
RegistryOperation, SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, YamlImportJob, YamlImportJobCompletion, YamlImportJobId,
|
||||
YamlImportJobStatus,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest, DescriptorKind,
|
||||
DescriptorMetadata, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PublishRequest, RegistryOperation, SampleKind, SaveAuthProfileRequest,
|
||||
SaveDescriptorMetadataRequest, SaveSampleMetadataRequest, UpdateWorkspaceRequest,
|
||||
WorkspaceRecord, YamlImportJob, YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
};
|
||||
pub use postgres::PostgresRegistry;
|
||||
|
||||
@@ -1,10 +1,48 @@
|
||||
use sqlx::{PgPool, query};
|
||||
|
||||
pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
query(
|
||||
"create table if not exists workspaces (
|
||||
id text primary key,
|
||||
slug text not null unique,
|
||||
display_name text not null,
|
||||
status text not null,
|
||||
settings_json jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null,
|
||||
updated_at timestamptz not null
|
||||
)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"insert into workspaces (
|
||||
id,
|
||||
slug,
|
||||
display_name,
|
||||
status,
|
||||
settings_json,
|
||||
created_at,
|
||||
updated_at
|
||||
) values (
|
||||
'ws_default',
|
||||
'default',
|
||||
'Default Workspace',
|
||||
'active',
|
||||
'{}'::jsonb,
|
||||
now(),
|
||||
now()
|
||||
)
|
||||
on conflict (id) do nothing",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists operations (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
workspace_id text null references workspaces(id) on delete cascade,
|
||||
name text not null,
|
||||
display_name text not null,
|
||||
protocol text not null,
|
||||
status text not null,
|
||||
@@ -18,6 +56,24 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query("alter table operations add column if not exists workspace_id text null references workspaces(id) on delete cascade")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("update operations set workspace_id = 'ws_default' where workspace_id is null")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("alter table operations alter column workspace_id set not null")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("alter table operations drop constraint if exists operations_name_key")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query(
|
||||
"create unique index if not exists operations_workspace_name_idx on operations(workspace_id, name)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists operation_versions (
|
||||
operation_id text not null references operations(id) on delete cascade,
|
||||
@@ -89,7 +145,8 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
query(
|
||||
"create table if not exists auth_profiles (
|
||||
id text primary key,
|
||||
name text not null unique,
|
||||
workspace_id text null references workspaces(id) on delete cascade,
|
||||
name text not null,
|
||||
kind text not null,
|
||||
config_json jsonb not null,
|
||||
created_at timestamptz not null,
|
||||
@@ -99,6 +156,24 @@ pub async fn apply_postgres(pool: &PgPool) -> Result<(), sqlx::Error> {
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query("alter table auth_profiles add column if not exists workspace_id text null references workspaces(id) on delete cascade")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("update auth_profiles set workspace_id = 'ws_default' where workspace_id is null")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("alter table auth_profiles alter column workspace_id set not null")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query("alter table auth_profiles drop constraint if exists auth_profiles_name_key")
|
||||
.execute(pool)
|
||||
.await?;
|
||||
query(
|
||||
"create unique index if not exists auth_profiles_workspace_name_idx on auth_profiles(workspace_id, name)",
|
||||
)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
||||
query(
|
||||
"create table if not exists yaml_import_jobs (
|
||||
id text primary key,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crank_core::{
|
||||
AuthProfile, DescriptorId, ExportMode, Operation, OperationId, OperationStatus, Protocol,
|
||||
SampleId,
|
||||
SampleId, Workspace, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::MappingSet;
|
||||
use crank_schema::Schema;
|
||||
@@ -40,9 +40,15 @@ define_registry_id!(YamlImportJobId);
|
||||
|
||||
pub type RegistryOperation = Operation<Schema, MappingSet>;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct WorkspaceRecord {
|
||||
pub workspace: Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct OperationSummary {
|
||||
pub id: OperationId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub name: String,
|
||||
pub display_name: String,
|
||||
pub protocol: Protocol,
|
||||
@@ -57,6 +63,7 @@ pub struct OperationSummary {
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct OperationVersionRecord {
|
||||
pub operation_id: OperationId,
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub version: u32,
|
||||
pub status: OperationStatus,
|
||||
pub change_note: Option<String>,
|
||||
@@ -138,6 +145,7 @@ pub struct YamlImportJobCompletion {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CreateVersionRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub snapshot: &'a RegistryOperation,
|
||||
pub change_note: Option<&'a str>,
|
||||
pub created_by: Option<&'a str>,
|
||||
@@ -145,6 +153,7 @@ pub struct CreateVersionRequest<'a> {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct PublishRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub operation_id: &'a OperationId,
|
||||
pub version: u32,
|
||||
pub published_at: &'a str,
|
||||
@@ -162,9 +171,20 @@ pub struct CreateYamlImportJobRequest<'a> {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SaveAuthProfileRequest<'a> {
|
||||
pub workspace_id: &'a WorkspaceId,
|
||||
pub profile: &'a AuthProfile,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CreateWorkspaceRequest<'a> {
|
||||
pub workspace: &'a Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UpdateWorkspaceRequest<'a> {
|
||||
pub workspace: &'a Workspace,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct SaveSampleMetadataRequest<'a> {
|
||||
pub sample: &'a OperationSampleMetadata,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crank_core::{AuthProfile, OperationId, OperationStatus};
|
||||
use crank_core::{AuthProfile, OperationId, OperationStatus, Workspace, WorkspaceId};
|
||||
use serde::{Serialize, de::DeserializeOwned};
|
||||
use serde_json::Value;
|
||||
use sqlx::{
|
||||
@@ -11,11 +11,11 @@ use crate::{
|
||||
error::RegistryError,
|
||||
migrations,
|
||||
model::{
|
||||
CreateVersionRequest, CreateYamlImportJobRequest, DescriptorMetadata,
|
||||
OperationSampleMetadata, OperationSummary, OperationVersionRecord, PublishRequest,
|
||||
RegistryOperation, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, YamlImportJob, YamlImportJobCompletion, YamlImportJobId,
|
||||
YamlImportJobStatus,
|
||||
CreateVersionRequest, CreateWorkspaceRequest, CreateYamlImportJobRequest,
|
||||
DescriptorMetadata, OperationSampleMetadata, OperationSummary, OperationVersionRecord,
|
||||
PublishRequest, RegistryOperation, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
|
||||
SaveSampleMetadataRequest, UpdateWorkspaceRequest, WorkspaceRecord, YamlImportJob,
|
||||
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -38,6 +38,126 @@ impl PostgresRegistry {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
slug,
|
||||
display_name,
|
||||
status,
|
||||
settings_json,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
|
||||
from workspaces
|
||||
order by slug asc",
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
rows.iter().map(map_workspace_record).collect()
|
||||
}
|
||||
|
||||
pub async fn get_workspace(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Option<WorkspaceRecord>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
slug,
|
||||
display_name,
|
||||
status,
|
||||
settings_json,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
|
||||
from workspaces
|
||||
where id = $1",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
|
||||
row.as_ref().map(map_workspace_record).transpose()
|
||||
}
|
||||
|
||||
pub async fn create_workspace(
|
||||
&self,
|
||||
request: CreateWorkspaceRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result = sqlx::query(
|
||||
"insert into workspaces (
|
||||
id,
|
||||
slug,
|
||||
display_name,
|
||||
status,
|
||||
settings_json,
|
||||
created_at,
|
||||
updated_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(request.workspace.id.as_str())
|
||||
.bind(&request.workspace.slug)
|
||||
.bind(&request.workspace.display_name)
|
||||
.bind(serialize_enum_text(&request.workspace.status, "status")?)
|
||||
.bind(Json(request.workspace.settings.clone()))
|
||||
.bind(&request.workspace.created_at)
|
||||
.bind(&request.workspace.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => Ok(()),
|
||||
Err(sqlx::Error::Database(error))
|
||||
if error.constraint() == Some("workspaces_slug_key") =>
|
||||
{
|
||||
Err(RegistryError::WorkspaceSlugAlreadyExists {
|
||||
slug: request.workspace.slug.clone(),
|
||||
})
|
||||
}
|
||||
Err(error) => Err(RegistryError::Storage(error)),
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn update_workspace(
|
||||
&self,
|
||||
request: UpdateWorkspaceRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result = sqlx::query(
|
||||
"update workspaces
|
||||
set slug = $1,
|
||||
display_name = $2,
|
||||
status = $3,
|
||||
settings_json = $4,
|
||||
updated_at = $5::timestamptz
|
||||
where id = $6",
|
||||
)
|
||||
.bind(&request.workspace.slug)
|
||||
.bind(&request.workspace.display_name)
|
||||
.bind(serialize_enum_text(&request.workspace.status, "status")?)
|
||||
.bind(Json(request.workspace.settings.clone()))
|
||||
.bind(&request.workspace.updated_at)
|
||||
.bind(request.workspace.id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(result) if result.rows_affected() == 0 => Err(RegistryError::WorkspaceNotFound {
|
||||
workspace_id: request.workspace.id.as_str().to_owned(),
|
||||
}),
|
||||
Ok(_) => Ok(()),
|
||||
Err(sqlx::Error::Database(error))
|
||||
if error.constraint() == Some("workspaces_slug_key") =>
|
||||
{
|
||||
Err(RegistryError::WorkspaceSlugAlreadyExists {
|
||||
slug: request.workspace.slug.clone(),
|
||||
})
|
||||
}
|
||||
Err(error) => Err(RegistryError::Storage(error)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_in_schema(
|
||||
database_url: &str,
|
||||
schema: Option<&str>,
|
||||
@@ -60,6 +180,7 @@ impl PostgresRegistry {
|
||||
|
||||
pub async fn create_operation(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
snapshot: &RegistryOperation,
|
||||
created_by: Option<&str>,
|
||||
) -> Result<(), RegistryError> {
|
||||
@@ -70,7 +191,11 @@ impl PostgresRegistry {
|
||||
});
|
||||
}
|
||||
|
||||
if self.get_operation_summary(&snapshot.id).await?.is_some() {
|
||||
if self
|
||||
.get_operation_summary(workspace_id, &snapshot.id)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(RegistryError::OperationAlreadyExists {
|
||||
operation_id: snapshot.id.as_str().to_owned(),
|
||||
});
|
||||
@@ -81,6 +206,7 @@ impl PostgresRegistry {
|
||||
sqlx::query(
|
||||
"insert into operations (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
display_name,
|
||||
protocol,
|
||||
@@ -91,13 +217,14 @@ impl PostgresRegistry {
|
||||
updated_at,
|
||||
published_at
|
||||
) values (
|
||||
$1, $2, $3, $4, $5, $6, $7,
|
||||
$8::timestamptz,
|
||||
$1, $2, $3, $4, $5, $6, $7, $8,
|
||||
$9::timestamptz,
|
||||
$10::timestamptz
|
||||
$10::timestamptz,
|
||||
$11::timestamptz
|
||||
)",
|
||||
)
|
||||
.bind(snapshot.id.as_str())
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(&snapshot.name)
|
||||
.bind(&snapshot.display_name)
|
||||
.bind(serialize_enum_text(&snapshot.protocol, "protocol")?)
|
||||
@@ -125,7 +252,10 @@ impl PostgresRegistry {
|
||||
&self,
|
||||
request: CreateVersionRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
let Some(summary) = self.get_operation_summary(&request.snapshot.id).await? else {
|
||||
let Some(summary) = self
|
||||
.get_operation_summary(request.workspace_id, &request.snapshot.id)
|
||||
.await?
|
||||
else {
|
||||
return Err(RegistryError::OperationNotFound {
|
||||
operation_id: request.snapshot.id.as_str().to_owned(),
|
||||
});
|
||||
@@ -170,10 +300,14 @@ impl PostgresRegistry {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn list_operations(&self) -> Result<Vec<OperationSummary>, RegistryError> {
|
||||
pub async fn list_operations(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<OperationSummary>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
display_name,
|
||||
protocol,
|
||||
@@ -184,8 +318,10 @@ impl PostgresRegistry {
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from operations
|
||||
where workspace_id = $1
|
||||
order by name asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -194,11 +330,13 @@ impl PostgresRegistry {
|
||||
|
||||
pub async fn get_operation_summary(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<Option<OperationSummary>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
display_name,
|
||||
protocol,
|
||||
@@ -209,8 +347,9 @@ impl PostgresRegistry {
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
|
||||
to_char(published_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as published_at
|
||||
from operations
|
||||
where id = $1",
|
||||
where workspace_id = $1 and id = $2",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(operation_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
@@ -220,12 +359,14 @@ impl PostgresRegistry {
|
||||
|
||||
pub async fn get_operation_version(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
version: u32,
|
||||
) -> Result<Option<OperationVersionRecord>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
o.id,
|
||||
o.workspace_id,
|
||||
o.name,
|
||||
o.display_name,
|
||||
o.protocol,
|
||||
@@ -249,8 +390,9 @@ impl PostgresRegistry {
|
||||
ov.created_by
|
||||
from operation_versions ov
|
||||
join operations o on o.id = ov.operation_id
|
||||
where ov.operation_id = $1 and ov.version = $2",
|
||||
where o.workspace_id = $1 and ov.operation_id = $2 and ov.version = $3",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(operation_id.as_str())
|
||||
.bind(to_db_version(version))
|
||||
.fetch_optional(&self.pool)
|
||||
@@ -261,11 +403,13 @@ impl PostgresRegistry {
|
||||
|
||||
pub async fn list_operation_versions(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
operation_id: &OperationId,
|
||||
) -> Result<Vec<OperationVersionRecord>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
o.id,
|
||||
o.workspace_id,
|
||||
o.name,
|
||||
o.display_name,
|
||||
o.protocol,
|
||||
@@ -289,9 +433,10 @@ impl PostgresRegistry {
|
||||
ov.created_by
|
||||
from operation_versions ov
|
||||
join operations o on o.id = ov.operation_id
|
||||
where ov.operation_id = $1
|
||||
where o.workspace_id = $1 and ov.operation_id = $2
|
||||
order by ov.version asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(operation_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
@@ -304,7 +449,7 @@ impl PostgresRegistry {
|
||||
request: PublishRequest<'_>,
|
||||
) -> Result<(), RegistryError> {
|
||||
if self
|
||||
.get_operation_version(request.operation_id, request.version)
|
||||
.get_operation_version(request.workspace_id, request.operation_id, request.version)
|
||||
.await?
|
||||
.is_none()
|
||||
{
|
||||
@@ -373,6 +518,7 @@ impl PostgresRegistry {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
o.id,
|
||||
o.workspace_id,
|
||||
o.name,
|
||||
o.display_name,
|
||||
o.protocol,
|
||||
@@ -413,6 +559,7 @@ impl PostgresRegistry {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
o.id,
|
||||
o.workspace_id,
|
||||
o.name,
|
||||
o.display_name,
|
||||
o.protocol,
|
||||
@@ -455,19 +602,22 @@ impl PostgresRegistry {
|
||||
sqlx::query(
|
||||
"insert into auth_profiles (
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
kind,
|
||||
config_json,
|
||||
created_at,
|
||||
updated_at
|
||||
) values ($1, $2, $3, $4, $5::timestamptz, $6::timestamptz)
|
||||
) values ($1, $2, $3, $4, $5, $6::timestamptz, $7::timestamptz)
|
||||
on conflict(id) do update set
|
||||
workspace_id = excluded.workspace_id,
|
||||
name = excluded.name,
|
||||
kind = excluded.kind,
|
||||
config_json = excluded.config_json,
|
||||
updated_at = excluded.updated_at",
|
||||
)
|
||||
.bind(request.profile.id.as_str())
|
||||
.bind(request.workspace_id.as_str())
|
||||
.bind(&request.profile.name)
|
||||
.bind(serialize_enum_text(&request.profile.kind, "kind")?)
|
||||
.bind(Json(serialize_json_value(&request.profile.config)?))
|
||||
@@ -481,19 +631,22 @@ impl PostgresRegistry {
|
||||
|
||||
pub async fn get_auth_profile(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
auth_profile_id: &crank_core::AuthProfileId,
|
||||
) -> Result<Option<AuthProfile>, RegistryError> {
|
||||
let row = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
kind,
|
||||
config_json,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
|
||||
from auth_profiles
|
||||
where id = $1",
|
||||
where workspace_id = $1 and id = $2",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.bind(auth_profile_id.as_str())
|
||||
.fetch_optional(&self.pool)
|
||||
.await?;
|
||||
@@ -501,18 +654,24 @@ impl PostgresRegistry {
|
||||
row.as_ref().map(map_auth_profile).transpose()
|
||||
}
|
||||
|
||||
pub async fn list_auth_profiles(&self) -> Result<Vec<AuthProfile>, RegistryError> {
|
||||
pub async fn list_auth_profiles(
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
) -> Result<Vec<AuthProfile>, RegistryError> {
|
||||
let rows = sqlx::query(
|
||||
"select
|
||||
id,
|
||||
workspace_id,
|
||||
name,
|
||||
kind,
|
||||
config_json,
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
|
||||
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at
|
||||
from auth_profiles
|
||||
where workspace_id = $1
|
||||
order by name asc",
|
||||
)
|
||||
.bind(workspace_id.as_str())
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -837,9 +996,24 @@ fn assert_immutable_fields(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn map_workspace_record(row: &PgRow) -> Result<WorkspaceRecord, RegistryError> {
|
||||
Ok(WorkspaceRecord {
|
||||
workspace: Workspace {
|
||||
id: WorkspaceId::new(row.try_get::<String, _>("id")?),
|
||||
slug: row.try_get("slug")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
|
||||
settings: row.try_get::<Json<Value>, _>("settings_json")?.0,
|
||||
created_at: row.try_get("created_at")?,
|
||||
updated_at: row.try_get("updated_at")?,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn map_operation_summary(row: &PgRow) -> Result<OperationSummary, RegistryError> {
|
||||
Ok(OperationSummary {
|
||||
id: OperationId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
name: row.try_get("name")?,
|
||||
display_name: row.try_get("display_name")?,
|
||||
protocol: deserialize_enum_text(&row.try_get::<String, _>("protocol")?, "protocol")?,
|
||||
@@ -865,6 +1039,7 @@ fn map_operation_version_record(row: &PgRow) -> Result<OperationVersionRecord, R
|
||||
|
||||
Ok(OperationVersionRecord {
|
||||
operation_id: operation_id.clone(),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
version,
|
||||
status,
|
||||
change_note: row.try_get("change_note")?,
|
||||
@@ -918,6 +1093,7 @@ fn map_operation_version_record(row: &PgRow) -> Result<OperationVersionRecord, R
|
||||
fn map_auth_profile(row: &PgRow) -> Result<AuthProfile, RegistryError> {
|
||||
Ok(AuthProfile {
|
||||
id: crank_core::AuthProfileId::new(row.try_get::<String, _>("id")?),
|
||||
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
|
||||
name: row.try_get("name")?,
|
||||
kind: deserialize_enum_text(&row.try_get::<String, _>("kind")?, "kind")?,
|
||||
config: deserialize_json_value(row.try_get::<Json<Value>, _>("config_json")?.0)?,
|
||||
@@ -1042,7 +1218,7 @@ mod tests {
|
||||
ApiKeyHeaderAuthConfig, AuthConfig, AuthKind, AuthProfile, ConfigExport, ExecutionConfig,
|
||||
ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, OperationId, OperationStatus,
|
||||
Protocol, RestTarget, RetryPolicy, Samples, SecretRef, Target, ToolDescription,
|
||||
ToolExample,
|
||||
ToolExample, WorkspaceId,
|
||||
};
|
||||
use crank_mapping::{MappingRule, MappingSet};
|
||||
use crank_schema::{Schema, SchemaKind};
|
||||
@@ -1059,6 +1235,10 @@ mod tests {
|
||||
},
|
||||
};
|
||||
|
||||
fn test_workspace_id() -> WorkspaceId {
|
||||
WorkspaceId::new("ws_default")
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn stores_versions_and_published_operations() {
|
||||
let database = TestDatabase::new().await;
|
||||
@@ -1066,7 +1246,7 @@ mod tests {
|
||||
let operation_v1 = test_operation("op_rest_01", 1, OperationStatus::Draft);
|
||||
|
||||
registry
|
||||
.create_operation(&operation_v1, Some("alice"))
|
||||
.create_operation(&test_workspace_id(), &operation_v1, Some("alice"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -1074,6 +1254,7 @@ mod tests {
|
||||
|
||||
registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &operation_v2,
|
||||
change_note: Some("add output mapping"),
|
||||
created_by: Some("alice"),
|
||||
@@ -1083,6 +1264,7 @@ mod tests {
|
||||
|
||||
registry
|
||||
.publish_operation(PublishRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
operation_id: &operation_v2.id,
|
||||
version: operation_v2.version,
|
||||
published_at: "2026-03-25T12:10:00Z",
|
||||
@@ -1092,12 +1274,12 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let summary = registry
|
||||
.get_operation_summary(&operation_v2.id)
|
||||
.get_operation_summary(&test_workspace_id(), &operation_v2.id)
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let versions = registry
|
||||
.list_operation_versions(&operation_v2.id)
|
||||
.list_operation_versions(&test_workspace_id(), &operation_v2.id)
|
||||
.await
|
||||
.unwrap();
|
||||
let published = registry
|
||||
@@ -1126,11 +1308,15 @@ mod tests {
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_rest_02", 1, OperationStatus::Draft);
|
||||
|
||||
registry.create_operation(&operation, None).await.unwrap();
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let invalid = test_operation("op_rest_02", 3, OperationStatus::Draft);
|
||||
let error = registry
|
||||
.create_version(CreateVersionRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
snapshot: &invalid,
|
||||
change_note: None,
|
||||
created_by: None,
|
||||
@@ -1156,10 +1342,14 @@ mod tests {
|
||||
let registry = database.registry().await;
|
||||
let operation = test_operation("op_rest_03", 1, OperationStatus::Draft);
|
||||
|
||||
registry.create_operation(&operation, None).await.unwrap();
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_profile = AuthProfile {
|
||||
id: "auth_crank".into(),
|
||||
workspace_id: test_workspace_id(),
|
||||
name: "Crank API key".to_owned(),
|
||||
kind: AuthKind::ApiKeyHeader,
|
||||
config: AuthConfig::ApiKeyHeader(ApiKeyHeaderAuthConfig {
|
||||
@@ -1172,6 +1362,7 @@ mod tests {
|
||||
|
||||
registry
|
||||
.save_auth_profile(SaveAuthProfileRequest {
|
||||
workspace_id: &test_workspace_id(),
|
||||
profile: &auth_profile,
|
||||
})
|
||||
.await
|
||||
@@ -1211,7 +1402,10 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let auth_profiles = registry.list_auth_profiles().await.unwrap();
|
||||
let auth_profiles = registry
|
||||
.list_auth_profiles(&test_workspace_id())
|
||||
.await
|
||||
.unwrap();
|
||||
let samples = registry
|
||||
.list_sample_metadata(&operation.id, 1)
|
||||
.await
|
||||
@@ -1235,7 +1429,10 @@ mod tests {
|
||||
let job_id = YamlImportJobId::new("job_yaml_01");
|
||||
let operation = test_operation("op_rest_04", 1, OperationStatus::Draft);
|
||||
|
||||
registry.create_operation(&operation, None).await.unwrap();
|
||||
registry
|
||||
.create_operation(&test_workspace_id(), &operation, None)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
registry
|
||||
.create_yaml_import_job(CreateYamlImportJobRequest {
|
||||
|
||||
Reference in New Issue
Block a user