core: type workspace and secret timestamps
This commit is contained in:
@@ -58,7 +58,8 @@ pub struct Membership {
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub user_id: UserId,
|
||||
pub role: MembershipRole,
|
||||
pub created_at: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -69,8 +70,10 @@ pub struct InvitationToken {
|
||||
pub role: MembershipRole,
|
||||
pub status: InvitationStatus,
|
||||
pub token_hash: String,
|
||||
pub expires_at: String,
|
||||
pub created_at: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub expires_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::ids::{SecretId, UserId, WorkspaceId};
|
||||
|
||||
@@ -26,9 +27,12 @@ pub struct Secret {
|
||||
pub kind: SecretKind,
|
||||
pub status: SecretStatus,
|
||||
pub current_version: u32,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
pub last_used_at: Option<String>,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339::option")]
|
||||
pub last_used_at: Option<OffsetDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -37,6 +41,56 @@ pub struct SecretVersion {
|
||||
pub version: u32,
|
||||
pub ciphertext: String,
|
||||
pub key_version: String,
|
||||
pub created_at: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
pub created_by: Option<UserId>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use super::{Secret, SecretKind, SecretStatus, SecretVersion};
|
||||
use crate::ids::{SecretId, UserId, WorkspaceId};
|
||||
|
||||
#[test]
|
||||
fn secret_serializes_timestamps_as_rfc3339() {
|
||||
let secret = Secret {
|
||||
id: SecretId::new("secret_01"),
|
||||
workspace_id: WorkspaceId::new("ws_01"),
|
||||
name: "primary".to_owned(),
|
||||
kind: SecretKind::Token,
|
||||
status: SecretStatus::Active,
|
||||
current_version: 2,
|
||||
created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(),
|
||||
updated_at: OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap(),
|
||||
last_used_at: Some(OffsetDateTime::parse("2026-03-25T12:07:00Z", &Rfc3339).unwrap()),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&secret).unwrap();
|
||||
|
||||
assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z"));
|
||||
assert_eq!(value["updated_at"], json!("2026-03-25T12:05:00Z"));
|
||||
assert_eq!(value["last_used_at"], json!("2026-03-25T12:07:00Z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn secret_version_deserializes_created_at_from_rfc3339() {
|
||||
let version: SecretVersion = serde_json::from_value(json!({
|
||||
"secret_id": "secret_01",
|
||||
"version": 2,
|
||||
"ciphertext": "opaque",
|
||||
"key_version": "v2",
|
||||
"created_at": "2026-03-25T12:07:00Z",
|
||||
"created_by": "user_01"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
version.created_at,
|
||||
OffsetDateTime::parse("2026-03-25T12:07:00Z", &Rfc3339).unwrap()
|
||||
);
|
||||
assert_eq!(version.created_by, Some(UserId::new("user_01")));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
use crate::ids::WorkspaceId;
|
||||
|
||||
@@ -17,6 +18,58 @@ pub struct Workspace {
|
||||
pub display_name: String,
|
||||
pub status: WorkspaceStatus,
|
||||
pub settings: Value,
|
||||
pub created_at: String,
|
||||
pub updated_at: String,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub created_at: OffsetDateTime,
|
||||
#[serde(with = "time::serde::rfc3339")]
|
||||
pub updated_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use time::{OffsetDateTime, format_description::well_known::Rfc3339};
|
||||
|
||||
use super::{Workspace, WorkspaceStatus};
|
||||
use crate::ids::WorkspaceId;
|
||||
|
||||
#[test]
|
||||
fn workspace_serializes_timestamps_as_rfc3339() {
|
||||
let workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_01"),
|
||||
slug: "primary".to_owned(),
|
||||
display_name: "Primary".to_owned(),
|
||||
status: WorkspaceStatus::Active,
|
||||
settings: json!({"region":"eu"}),
|
||||
created_at: OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap(),
|
||||
updated_at: OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap(),
|
||||
};
|
||||
|
||||
let value = serde_json::to_value(&workspace).unwrap();
|
||||
|
||||
assert_eq!(value["created_at"], json!("2026-03-25T12:00:00Z"));
|
||||
assert_eq!(value["updated_at"], json!("2026-03-25T12:05:00Z"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn workspace_deserializes_timestamps_from_rfc3339() {
|
||||
let workspace: Workspace = serde_json::from_value(json!({
|
||||
"id": "ws_01",
|
||||
"slug": "primary",
|
||||
"display_name": "Primary",
|
||||
"status": "active",
|
||||
"settings": {"region":"eu"},
|
||||
"created_at": "2026-03-25T12:00:00Z",
|
||||
"updated_at": "2026-03-25T12:05:00Z"
|
||||
}))
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
workspace.created_at,
|
||||
OffsetDateTime::parse("2026-03-25T12:00:00Z", &Rfc3339).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
workspace.updated_at,
|
||||
OffsetDateTime::parse("2026-03-25T12:05:00Z", &Rfc3339).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,7 +61,7 @@ pub struct MembershipRecord {
|
||||
pub workspace_id: WorkspaceId,
|
||||
pub user: User,
|
||||
pub role: MembershipRole,
|
||||
pub created_at: String,
|
||||
pub created_at: OffsetDateTime,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
@@ -448,8 +448,8 @@ pub struct RotateSecretRequest<'a> {
|
||||
pub secret_id: &'a SecretId,
|
||||
pub ciphertext: &'a str,
|
||||
pub key_version: &'a str,
|
||||
pub created_at: &'a str,
|
||||
pub updated_at: &'a str,
|
||||
pub created_at: &'a OffsetDateTime,
|
||||
pub updated_at: &'a OffsetDateTime,
|
||||
pub created_by: Option<&'a crank_core::UserId>,
|
||||
}
|
||||
|
||||
|
||||
@@ -1478,8 +1478,8 @@ mod tests {
|
||||
display_name: "Extra Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({"region":"eu"}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let mut user = User {
|
||||
id: UserId::new("user_extra_01"),
|
||||
@@ -1544,8 +1544,8 @@ mod tests {
|
||||
display_name: "Profile Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let other_workspace = Workspace {
|
||||
id: WorkspaceId::new("ws_profile_02"),
|
||||
@@ -1553,8 +1553,8 @@ mod tests {
|
||||
display_name: "Other Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: "2026-03-25T12:01:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:01:00Z".to_owned(),
|
||||
created_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:01:00Z"),
|
||||
};
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user("profile@example.com", "Owner", "hashed-password")
|
||||
@@ -1611,8 +1611,8 @@ mod tests {
|
||||
display_name: "Session Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let user_id = registry
|
||||
.upsert_bootstrap_user("session@example.com", "Owner", "hashed-password")
|
||||
@@ -1666,8 +1666,8 @@ mod tests {
|
||||
display_name: "Keys Workspace".to_owned(),
|
||||
status: crank_core::WorkspaceStatus::Active,
|
||||
settings: json!({}),
|
||||
created_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
updated_at: "2026-03-25T12:00:00Z".to_owned(),
|
||||
created_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
updated_at: timestamp("2026-03-25T12:00:00Z"),
|
||||
};
|
||||
let api_key = PlatformApiKey {
|
||||
id: PlatformApiKeyId::new("key_01"),
|
||||
|
||||
@@ -13,9 +13,9 @@ impl PostgresRegistry {
|
||||
kind,
|
||||
status,
|
||||
current_version,
|
||||
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!\",
|
||||
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
|
||||
created_at as \"created_at!: time::OffsetDateTime\",
|
||||
updated_at as \"updated_at!: time::OffsetDateTime\",
|
||||
last_used_at as \"last_used_at: time::OffsetDateTime\"
|
||||
from secrets
|
||||
where workspace_id = $1
|
||||
order by name asc",
|
||||
@@ -56,9 +56,9 @@ impl PostgresRegistry {
|
||||
kind,
|
||||
status,
|
||||
current_version,
|
||||
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!\",
|
||||
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
|
||||
created_at as \"created_at!: time::OffsetDateTime\",
|
||||
updated_at as \"updated_at!: time::OffsetDateTime\",
|
||||
last_used_at as \"last_used_at: time::OffsetDateTime\"
|
||||
from secrets
|
||||
where workspace_id = $1 and id = $2",
|
||||
workspace_id.as_str(),
|
||||
@@ -96,7 +96,7 @@ impl PostgresRegistry {
|
||||
sv.version,
|
||||
sv.ciphertext,
|
||||
sv.key_version,
|
||||
to_char(sv.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
|
||||
sv.created_at as \"created_at!: time::OffsetDateTime\",
|
||||
sv.created_by
|
||||
from secrets s
|
||||
join secret_versions sv
|
||||
@@ -149,9 +149,9 @@ impl PostgresRegistry {
|
||||
.bind(serialize_enum_text(&request.secret.kind, "kind")?)
|
||||
.bind(serialize_enum_text(&request.secret.status, "status")?)
|
||||
.bind(to_db_version(request.secret.current_version))
|
||||
.bind(request.secret.last_used_at.as_deref())
|
||||
.bind(&request.secret.created_at)
|
||||
.bind(&request.secret.updated_at)
|
||||
.bind(request.secret.last_used_at)
|
||||
.bind(request.secret.created_at)
|
||||
.bind(request.secret.updated_at)
|
||||
.execute(&mut *tx)
|
||||
.await;
|
||||
|
||||
@@ -173,7 +173,7 @@ impl PostgresRegistry {
|
||||
.bind(to_db_version(request.secret.current_version))
|
||||
.bind(request.ciphertext)
|
||||
.bind(request.key_version)
|
||||
.bind(&request.secret.created_at)
|
||||
.bind(request.secret.created_at)
|
||||
.bind(request.created_by.map(|value| value.as_str()))
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
@@ -247,7 +247,7 @@ impl PostgresRegistry {
|
||||
version: next_version,
|
||||
ciphertext: request.ciphertext.to_owned(),
|
||||
key_version: request.key_version.to_owned(),
|
||||
created_at: request.created_at.to_owned(),
|
||||
created_at: *request.created_at,
|
||||
created_by: request.created_by.cloned(),
|
||||
},
|
||||
})
|
||||
@@ -280,7 +280,7 @@ impl PostgresRegistry {
|
||||
&self,
|
||||
workspace_id: &WorkspaceId,
|
||||
secret_id: &SecretId,
|
||||
used_at: &str,
|
||||
used_at: &OffsetDateTime,
|
||||
) -> Result<(), RegistryError> {
|
||||
let result = sqlx::query(
|
||||
"update secrets
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use time::OffsetDateTime;
|
||||
|
||||
impl PostgresRegistry {
|
||||
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, RegistryError> {
|
||||
@@ -10,8 +9,8 @@ impl PostgresRegistry {
|
||||
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!\"
|
||||
created_at as \"created_at!: time::OffsetDateTime\",
|
||||
updated_at as \"updated_at!: time::OffsetDateTime\"
|
||||
from workspaces
|
||||
order by slug asc",
|
||||
)
|
||||
@@ -46,8 +45,8 @@ impl PostgresRegistry {
|
||||
w.display_name,
|
||||
w.status,
|
||||
w.settings_json,
|
||||
to_char(w.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
|
||||
to_char(w.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",
|
||||
w.created_at as \"created_at!: time::OffsetDateTime\",
|
||||
w.updated_at as \"updated_at!: time::OffsetDateTime\",
|
||||
m.role
|
||||
from memberships m
|
||||
join workspaces w on w.id = m.workspace_id
|
||||
@@ -85,11 +84,11 @@ impl PostgresRegistry {
|
||||
m.workspace_id,
|
||||
m.user_id,
|
||||
m.role,
|
||||
to_char(m.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
|
||||
m.created_at as \"created_at!: time::OffsetDateTime\",
|
||||
u.email,
|
||||
u.display_name,
|
||||
u.status,
|
||||
u.created_at as \"user_created_at!: OffsetDateTime\"
|
||||
u.created_at as \"user_created_at!: time::OffsetDateTime\"
|
||||
from memberships m
|
||||
join users u on u.id = m.user_id
|
||||
where m.workspace_id = $1
|
||||
@@ -180,8 +179,8 @@ impl PostgresRegistry {
|
||||
role,
|
||||
status,
|
||||
token_hash,
|
||||
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",
|
||||
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"
|
||||
expires_at as \"expires_at!: time::OffsetDateTime\",
|
||||
created_at as \"created_at!: time::OffsetDateTime\"
|
||||
from invitation_tokens
|
||||
where workspace_id = $1
|
||||
order by created_at desc",
|
||||
@@ -232,8 +231,8 @@ impl PostgresRegistry {
|
||||
.bind(serialize_enum_text(&request.invitation.role, "role")?)
|
||||
.bind(serialize_enum_text(&request.invitation.status, "status")?)
|
||||
.bind(&request.invitation.token_hash)
|
||||
.bind(&request.invitation.expires_at)
|
||||
.bind(&request.invitation.created_at)
|
||||
.bind(request.invitation.expires_at)
|
||||
.bind(request.invitation.created_at)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
|
||||
@@ -287,8 +286,8 @@ impl PostgresRegistry {
|
||||
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!\"
|
||||
created_at as \"created_at!: time::OffsetDateTime\",
|
||||
updated_at as \"updated_at!: time::OffsetDateTime\"
|
||||
from workspaces
|
||||
where id = $1",
|
||||
workspace_id.as_str(),
|
||||
@@ -334,8 +333,8 @@ impl PostgresRegistry {
|
||||
.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)
|
||||
.bind(request.workspace.created_at)
|
||||
.bind(request.workspace.updated_at)
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
@@ -369,7 +368,7 @@ impl PostgresRegistry {
|
||||
.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.updated_at)
|
||||
.bind(request.workspace.id.as_str())
|
||||
.execute(&self.pool)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user