core: type workspace and secret timestamps

This commit is contained in:
a.tolmachev
2026-04-13 05:59:11 +00:00
parent 03cb109b40
commit c736b4f5a8
17 changed files with 243 additions and 139 deletions
+6 -3
View File
@@ -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)]
+58 -4
View File
@@ -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")));
}
}
+55 -2
View File
@@ -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()
);
}
}
+3 -3
View File
@@ -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>,
}
+10 -10
View File
@@ -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 -13
View File
@@ -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
+15 -16
View File
@@ -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;