registry: add sqlx checks for workspace and stream reads

This commit is contained in:
a.tolmachev
2026-04-12 20:49:12 +00:00
parent 57c73b5cb2
commit 6a909feb64
15 changed files with 1195 additions and 151 deletions
@@ -0,0 +1,56 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n slug,\n display_name,\n status,\n settings_json,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\"\n from workspaces\n order by slug asc",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "slug",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "display_name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "settings_json",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "updated_at!",
"type_info": "Text"
}
],
"parameters": {
"Left": []
},
"nullable": [
false,
false,
false,
false,
false,
null,
null
]
},
"hash": "0701c93547a515170803136ed093c06df78d08e83c9f2eee241204820fd20f5c"
}
@@ -0,0 +1,94 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n agent_id,\n operation_id,\n protocol,\n mode,\n status,\n cursor_json,\n state_json,\n to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",\n to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at\n from stream_sessions\n where id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "agent_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "operation_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "protocol",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "mode",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "cursor_json",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "state_json",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "expires_at!",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "last_poll_at",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "closed_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true,
false,
null,
null,
null,
null
]
},
"hash": "0c383ab1d4c5c178f9ded8219a81da145a4ba6198a67de5c7e060803619ad7fc"
}
@@ -0,0 +1,65 @@
{
"db_name": "PostgreSQL",
"query": "select\n k.id,\n k.workspace_id,\n k.name,\n k.prefix,\n k.scopes_json,\n k.status,\n to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(k.last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at\n from platform_api_keys k\n join workspaces w on w.id = k.workspace_id\n where w.slug = $1\n and k.secret_hash = $2\n and k.status = 'active'\n limit 1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "prefix",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "scopes_json",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "last_used_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
null,
null
]
},
"hash": "2b0b383bd209c6a94d6a422fbd52bf63338c048238c3ff63c44eddf6a56b8a0b"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "select\n w.id,\n w.slug,\n w.display_name,\n w.status,\n w.settings_json,\n to_char(w.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(w.updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",\n m.role\n from memberships m\n join workspaces w on w.id = m.workspace_id\n where m.user_id = $1\n order by w.slug asc",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "slug",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "display_name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "settings_json",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "updated_at!",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "role",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
null,
null,
false
]
},
"hash": "4206303b16bfbe2abc436d00ad5da3358246a78c0bb283321c9441e7eea68467"
}
@@ -0,0 +1,99 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n agent_id,\n operation_id,\n protocol,\n mode,\n status,\n cursor_json,\n state_json,\n to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",\n to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at\n from stream_sessions\n where workspace_id = $1\n and ($2::text is null or agent_id = $2)\n and ($3::text is null or operation_id = $3)\n and ($4::text is null or status = $4)\n and ($5::text is null or mode = $5)\n order by created_at desc\n limit $6",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "agent_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "operation_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "protocol",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "mode",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "cursor_json",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "state_json",
"type_info": "Jsonb"
},
{
"ordinal": 9,
"name": "expires_at!",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "last_poll_at",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 12,
"name": "closed_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text",
"Text",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
false,
true,
false,
null,
null,
null,
null
]
},
"hash": "4e2091cb75e67ac455b0efee795e94b8a18807a59a0f11796fec27519f34817d"
}
@@ -0,0 +1,92 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n agent_id,\n operation_id,\n status,\n progress_json,\n result_json,\n error_json,\n to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",\n to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at\n from async_jobs\n where workspace_id = $1\n and ($2::text is null or agent_id = $2)\n and ($3::text is null or operation_id = $3)\n and ($4::text is null or status = $4)\n order by updated_at desc\n limit $5",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "agent_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "operation_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "progress_json",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "result_json",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "error_json",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "expires_at",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "updated_at!",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "finished_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text",
"Text",
"Text",
"Text",
"Int8"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
true,
true,
null,
null,
null,
null
]
},
"hash": "6f55f9867b0fdaeedaa5995462e70e3b2118fb595c8d6e313b5f0271c0f5d537"
}
@@ -0,0 +1,58 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n slug,\n display_name,\n status,\n settings_json,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\"\n from workspaces\n where id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "slug",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "display_name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "settings_json",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "updated_at!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
null,
null
]
},
"hash": "925b63209407fe86b78bf5458b1a17253db129a7a13a6c7bc4db4f439244009c"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n email,\n role,\n status,\n token_hash,\n to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\"\n from invitation_tokens\n where workspace_id = $1\n order by created_at desc",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "role",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "token_hash",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "expires_at!",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "created_at!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
null,
null
]
},
"hash": "b69d55c4b0352cac007496828e2d08369749bac52d2076c3ba679c4aaa466fc9"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "select\n m.workspace_id,\n m.user_id,\n m.role,\n to_char(m.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n u.email,\n u.display_name,\n u.status,\n to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"user_created_at!\"\n from memberships m\n join users u on u.id = m.user_id\n where m.workspace_id = $1\n order by u.email asc",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "user_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "role",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "email",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "display_name",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "user_created_at!",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
null,
false,
false,
false,
null
]
},
"hash": "d485e119dd288962586c35b7585c74c6c3518f3d3995ea21a22cc35e092da147"
}
@@ -0,0 +1,88 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n agent_id,\n operation_id,\n status,\n progress_json,\n result_json,\n error_json,\n to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"updated_at!\",\n to_char(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at\n from async_jobs\n where id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "agent_id",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "operation_id",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 5,
"name": "progress_json",
"type_info": "Jsonb"
},
{
"ordinal": 6,
"name": "result_json",
"type_info": "Jsonb"
},
{
"ordinal": 7,
"name": "error_json",
"type_info": "Jsonb"
},
{
"ordinal": 8,
"name": "expires_at",
"type_info": "Text"
},
{
"ordinal": 9,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 10,
"name": "updated_at!",
"type_info": "Text"
},
{
"ordinal": 11,
"name": "finished_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
true,
false,
false,
false,
true,
true,
null,
null,
null,
null
]
},
"hash": "e92340e11c1b1d5dcd1441f12588e01fa18acc43e18316ea9789cd60e4967eaf"
}
@@ -0,0 +1,64 @@
{
"db_name": "PostgreSQL",
"query": "select\n id,\n workspace_id,\n name,\n prefix,\n scopes_json,\n status,\n to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",\n to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at\n from platform_api_keys\n where workspace_id = $1\n order by created_at desc",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "workspace_id",
"type_info": "Text"
},
{
"ordinal": 2,
"name": "name",
"type_info": "Text"
},
{
"ordinal": 3,
"name": "prefix",
"type_info": "Text"
},
{
"ordinal": 4,
"name": "scopes_json",
"type_info": "Jsonb"
},
{
"ordinal": 5,
"name": "status",
"type_info": "Text"
},
{
"ordinal": 6,
"name": "created_at!",
"type_info": "Text"
},
{
"ordinal": 7,
"name": "last_used_at",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
false,
false,
false,
false,
false,
false,
null,
null
]
},
"hash": "ed3c533baee2cdec1fabc3e3cece29939fee23fdf54dc4848a0951f3dd274f1f"
}
+38 -9
View File
@@ -5,7 +5,7 @@ impl PostgresRegistry {
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<PlatformApiKeyRecord>, RegistryError> {
let rows = sqlx::query(
let rows = sqlx::query!(
"select
id,
workspace_id,
@@ -13,17 +13,32 @@ impl PostgresRegistry {
prefix,
scopes_json,
status,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
from platform_api_keys
where workspace_id = $1
order by created_at desc",
workspace_id.as_str(),
)
.bind(workspace_id.as_str())
.fetch_all(&self.pool)
.await?;
rows.iter().map(map_platform_api_key_record).collect()
rows.into_iter()
.map(|row| {
Ok(PlatformApiKeyRecord {
api_key: PlatformApiKey {
id: PlatformApiKeyId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
prefix: row.prefix,
scopes: deserialize_json_value(row.scopes_json)?,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
last_used_at: row.last_used_at,
},
})
})
.collect()
}
pub async fn get_platform_api_key_by_secret_for_workspace_slug(
@@ -31,7 +46,7 @@ impl PostgresRegistry {
workspace_slug: &str,
secret_hash: &str,
) -> Result<Option<PlatformApiKeyRecord>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
k.id,
k.workspace_id,
@@ -39,7 +54,7 @@ impl PostgresRegistry {
k.prefix,
k.scopes_json,
k.status,
to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(k.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(k.last_used_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_used_at
from platform_api_keys k
join workspaces w on w.id = k.workspace_id
@@ -47,13 +62,27 @@ impl PostgresRegistry {
and k.secret_hash = $2
and k.status = 'active'
limit 1",
workspace_slug,
secret_hash,
)
.bind(workspace_slug)
.bind(secret_hash)
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_platform_api_key_record).transpose()
row.map(|row| {
Ok(PlatformApiKeyRecord {
api_key: PlatformApiKey {
id: PlatformApiKeyId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
name: row.name,
prefix: row.prefix,
scopes: deserialize_json_value(row.scopes_json)?,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.created_at,
last_used_at: row.last_used_at,
},
})
})
.transpose()
}
pub async fn create_platform_api_key(
+156 -85
View File
@@ -484,52 +484,6 @@ 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_workspace_membership_record(
row: &PgRow,
) -> Result<WorkspaceMembershipRecord, RegistryError> {
Ok(WorkspaceMembershipRecord {
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")?,
},
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
})
}
fn map_membership_record(row: &PgRow) -> Result<MembershipRecord, RegistryError> {
Ok(MembershipRecord {
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
user: User {
id: UserId::new(row.try_get::<String, _>("user_id")?),
email: row.try_get("email")?,
display_name: row.try_get("display_name")?,
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
created_at: row.try_get("user_created_at")?,
},
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
created_at: row.try_get("created_at")?,
})
}
fn map_user(row: &PgRow) -> Result<User, RegistryError> {
Ok(User {
id: UserId::new(row.try_get::<String, _>("id")?),
@@ -556,36 +510,6 @@ fn map_user_update_error(error: sqlx::Error, user_id: &UserId, email: &str) -> R
}
}
fn map_invitation_record(row: &PgRow) -> Result<InvitationRecord, RegistryError> {
Ok(InvitationRecord {
invitation: InvitationToken {
id: InvitationId::new(row.try_get::<String, _>("id")?),
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
email: row.try_get("email")?,
role: deserialize_enum_text(&row.try_get::<String, _>("role")?, "role")?,
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
token_hash: row.try_get("token_hash")?,
expires_at: row.try_get("expires_at")?,
created_at: row.try_get("created_at")?,
},
})
}
fn map_platform_api_key_record(row: &PgRow) -> Result<PlatformApiKeyRecord, RegistryError> {
Ok(PlatformApiKeyRecord {
api_key: PlatformApiKey {
id: PlatformApiKeyId::new(row.try_get::<String, _>("id")?),
workspace_id: WorkspaceId::new(row.try_get::<String, _>("workspace_id")?),
name: row.try_get("name")?,
prefix: row.try_get("prefix")?,
scopes: deserialize_json_value(row.try_get::<Json<Value>, _>("scopes_json")?.0)?,
status: deserialize_enum_text(&row.try_get::<String, _>("status")?, "status")?,
created_at: row.try_get("created_at")?,
last_used_at: row.try_get("last_used_at")?,
},
})
}
fn map_stream_session(row: &PgRow) -> Result<StreamSession, RegistryError> {
Ok(StreamSession {
id: StreamSessionId::new(row.try_get::<String, _>("id")?),
@@ -1124,9 +1048,10 @@ mod tests {
use crank_core::{
ApiKeyHeaderAuthConfig, AsyncJobHandle, AuthConfig, AuthKind, AuthProfile, ConfigExport,
ExecutionConfig, ExportMode, GeneratedDraft, GeneratedDraftStatus, HttpMethod, JobStatus,
OperationId, OperationStatus, Protocol, RestTarget, RetryPolicy, Samples, SecretId,
StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription, ToolExample,
WorkspaceId,
MembershipRole, OperationId, OperationStatus, PlatformApiKey, PlatformApiKeyId,
PlatformApiKeyScope, PlatformApiKeyStatus, Protocol, RestTarget, RetryPolicy, Samples,
SecretId, StreamSession, StreamSessionId, StreamStatus, Target, ToolDescription,
ToolExample, User, UserId, Workspace, WorkspaceId,
};
use crank_mapping::{MappingRule, MappingSet};
use crank_schema::{Schema, SchemaKind};
@@ -1136,12 +1061,14 @@ mod tests {
use crate::{
PostgresRegistry, RegistryError,
model::{
AsyncJobFilter, CreateAsyncJobRequest, CreateStreamSessionRequest,
CreateVersionRequest, CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
OperationSampleMetadata, PublishRequest, RegistryOperation, SampleKind,
SaveAuthProfileRequest, SaveDescriptorMetadataRequest, SaveSampleMetadataRequest,
StreamSessionFilter, UpdateAsyncJobStatusRequest, UpdateStreamSessionStateRequest,
YamlImportJobCompletion, YamlImportJobId, YamlImportJobStatus,
AsyncJobFilter, CreateAsyncJobRequest, CreatePlatformApiKeyRequest,
CreateStreamSessionRequest, CreateVersionRequest, CreateWorkspaceRequest,
CreateYamlImportJobRequest, DescriptorKind, DescriptorMetadata,
OperationSampleMetadata, PlatformApiKeyRecord, PublishRequest, RegistryOperation,
SampleKind, SaveAuthProfileRequest, SaveDescriptorMetadataRequest,
SaveSampleMetadataRequest, StreamSessionFilter, UpdateAsyncJobStatusRequest,
UpdateStreamSessionStateRequest, WorkspaceRecord, YamlImportJobCompletion,
YamlImportJobId, YamlImportJobStatus,
},
};
@@ -1453,6 +1380,150 @@ mod tests {
database.cleanup().await;
}
#[tokio::test]
async fn manages_workspace_read_paths() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace = Workspace {
id: WorkspaceId::new("ws_extra_01"),
slug: "extra".to_owned(),
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(),
};
let mut user = User {
id: UserId::new("user_extra_01"),
email: "owner@example.com".to_owned(),
display_name: "Owner".to_owned(),
status: crank_core::UserStatus::Active,
created_at: "2026-03-25T11:00:00Z".to_owned(),
};
registry
.create_workspace(CreateWorkspaceRequest {
workspace: &workspace,
})
.await
.unwrap();
let user_id = registry
.upsert_bootstrap_user(&user.email, &user.display_name, "hashed-password")
.await
.unwrap();
user.id = user_id;
registry
.ensure_membership(&workspace.id, &user.id, MembershipRole::Owner)
.await
.unwrap();
let all_workspaces = registry.list_workspaces().await.unwrap();
let user_workspaces = registry.list_workspaces_for_user(&user.id).await.unwrap();
let memberships = registry.list_memberships(&workspace.id).await.unwrap();
let loaded = registry
.get_workspace(&workspace.id)
.await
.unwrap()
.unwrap();
assert!(
all_workspaces
.iter()
.any(|record| record.workspace == workspace)
);
assert_eq!(user_workspaces.len(), 1);
assert_eq!(user_workspaces[0].workspace, workspace);
assert_eq!(user_workspaces[0].role, MembershipRole::Owner);
assert_eq!(memberships.len(), 1);
assert_eq!(memberships[0].workspace_id, workspace.id);
assert_eq!(memberships[0].user.id, user.id);
assert_eq!(memberships[0].user.email, user.email);
assert_eq!(memberships[0].user.display_name, user.display_name);
assert_eq!(memberships[0].user.status, user.status);
assert_eq!(memberships[0].role, MembershipRole::Owner);
assert_eq!(loaded, WorkspaceRecord { workspace });
database.cleanup().await;
}
#[tokio::test]
async fn manages_platform_api_key_read_paths() {
let database = TestDatabase::new().await;
let registry = database.registry().await;
let workspace = Workspace {
id: WorkspaceId::new("ws_keys_01"),
slug: "keys".to_owned(),
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(),
};
let api_key = PlatformApiKey {
id: PlatformApiKeyId::new("key_01"),
workspace_id: workspace.id.clone(),
name: "Primary".to_owned(),
prefix: "crk_live".to_owned(),
scopes: vec![PlatformApiKeyScope::Read, PlatformApiKeyScope::Write],
status: PlatformApiKeyStatus::Active,
created_at: "2026-03-25T12:01:00Z".to_owned(),
last_used_at: None,
};
let secret_hash = "secret_hash_01";
registry
.create_workspace(CreateWorkspaceRequest {
workspace: &workspace,
})
.await
.unwrap();
registry
.create_platform_api_key(CreatePlatformApiKeyRequest {
api_key: &api_key,
secret_hash,
})
.await
.unwrap();
let listed = registry
.list_platform_api_keys(&workspace.id)
.await
.unwrap();
assert_eq!(
listed,
vec![PlatformApiKeyRecord {
api_key: api_key.clone()
}]
);
let resolved = registry
.get_platform_api_key_by_secret_for_workspace_slug(&workspace.slug, secret_hash)
.await
.unwrap()
.unwrap();
assert_eq!(resolved.api_key, api_key);
registry
.touch_platform_api_key(
&workspace.id,
&PlatformApiKeyId::new("key_01"),
"2026-03-25T12:05:00Z",
)
.await
.unwrap();
let touched = registry
.list_platform_api_keys(&workspace.id)
.await
.unwrap();
assert_eq!(
touched[0].api_key.last_used_at.as_deref(),
Some("2026-03-25T12:05:00Z")
);
database.cleanup().await;
}
#[tokio::test]
async fn manages_stream_sessions_with_transitions_and_cleanup() {
let database = TestDatabase::new().await;
+97 -33
View File
@@ -54,7 +54,7 @@ impl PostgresRegistry {
&self,
id: &StreamSessionId,
) -> Result<Option<StreamSession>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
id,
workspace_id,
@@ -65,18 +65,35 @@ impl PostgresRegistry {
status,
cursor_json,
state_json,
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",
to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at
from stream_sessions
where id = $1",
id.as_str(),
)
.bind(id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_stream_session).transpose()
row.map(|row| {
Ok(StreamSession {
id: StreamSessionId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
agent_id: row.agent_id.map(AgentId::new),
operation_id: OperationId::new(row.operation_id),
protocol: deserialize_enum_text(&row.protocol, "protocol")?,
mode: deserialize_enum_text(&row.mode, "mode")?,
status: deserialize_enum_text(&row.status, "status")?,
cursor: row.cursor_json,
state: row.state_json,
expires_at: row.expires_at,
last_poll_at: row.last_poll_at,
created_at: row.created_at,
closed_at: row.closed_at,
})
})
.transpose()
}
pub async fn update_stream_session_state(
@@ -196,7 +213,7 @@ impl PostgresRegistry {
.transpose()?;
let limit = i64::from(filter.limit);
let rows = sqlx::query(
let rows = sqlx::query!(
"select
id,
workspace_id,
@@ -207,9 +224,9 @@ impl PostgresRegistry {
status,
cursor_json,
state_json,
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as expires_at,
to_char(expires_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"expires_at!\",
to_char(last_poll_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as last_poll_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as created_at,
to_char(created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
to_char(closed_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as closed_at
from stream_sessions
where workspace_id = $1
@@ -219,20 +236,36 @@ impl PostgresRegistry {
and ($5::text is null or mode = $5)
order by created_at desc
limit $6",
filter.workspace_id.as_str(),
filter.agent_id.map(|value| value.as_str()),
filter.operation_id.map(|value| value.as_str()),
status,
mode,
limit,
)
.bind(filter.workspace_id.as_str())
.bind(filter.agent_id.map(|value| value.as_str()))
.bind(filter.operation_id.map(|value| value.as_str()))
.bind(status)
.bind(mode)
.bind(limit)
.fetch_all(&self.pool)
.await?;
let items = rows
.iter()
.map(map_stream_session)
.collect::<Result<Vec<_>, _>>()?;
.into_iter()
.map(|row| {
Ok(StreamSession {
id: StreamSessionId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
agent_id: row.agent_id.map(AgentId::new),
operation_id: OperationId::new(row.operation_id),
protocol: deserialize_enum_text(&row.protocol, "protocol")?,
mode: deserialize_enum_text(&row.mode, "mode")?,
status: deserialize_enum_text(&row.status, "status")?,
cursor: row.cursor_json,
state: row.state_json,
expires_at: row.expires_at,
last_poll_at: row.last_poll_at,
created_at: row.created_at,
closed_at: row.closed_at,
})
})
.collect::<Result<Vec<_>, RegistryError>>()?;
let total = items.len() as u64;
Ok(Page { items, total })
@@ -295,7 +328,7 @@ impl PostgresRegistry {
&self,
id: &AsyncJobId,
) -> Result<Option<AsyncJobHandle>, RegistryError> {
let row = sqlx::query(
let row = sqlx::query!(
"select
id,
workspace_id,
@@ -306,17 +339,33 @@ impl PostgresRegistry {
result_json,
error_json,
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,
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
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(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at
from async_jobs
where id = $1",
id.as_str(),
)
.bind(id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_async_job).transpose()
row.map(|row| {
Ok(AsyncJobHandle {
id: AsyncJobId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
agent_id: row.agent_id.map(AgentId::new),
operation_id: OperationId::new(row.operation_id),
status: deserialize_enum_text(&row.status, "status")?,
progress: row.progress_json,
result: row.result_json,
error: row.error_json,
expires_at: row.expires_at,
created_at: row.created_at,
updated_at: row.updated_at,
finished_at: row.finished_at,
})
})
.transpose()
}
pub async fn update_async_job_status(
@@ -424,7 +473,7 @@ impl PostgresRegistry {
.transpose()?;
let limit = i64::from(filter.limit);
let rows = sqlx::query(
let rows = sqlx::query!(
"select
id,
workspace_id,
@@ -435,8 +484,8 @@ impl PostgresRegistry {
result_json,
error_json,
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,
to_char(updated_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as updated_at,
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(finished_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as finished_at
from async_jobs
where workspace_id = $1
@@ -445,19 +494,34 @@ impl PostgresRegistry {
and ($4::text is null or status = $4)
order by updated_at desc
limit $5",
filter.workspace_id.as_str(),
filter.agent_id.map(|value| value.as_str()),
filter.operation_id.map(|value| value.as_str()),
status,
limit,
)
.bind(filter.workspace_id.as_str())
.bind(filter.agent_id.map(|value| value.as_str()))
.bind(filter.operation_id.map(|value| value.as_str()))
.bind(status)
.bind(limit)
.fetch_all(&self.pool)
.await?;
let items = rows
.iter()
.map(map_async_job)
.collect::<Result<Vec<_>, _>>()?;
.into_iter()
.map(|row| {
Ok(AsyncJobHandle {
id: AsyncJobId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
agent_id: row.agent_id.map(AgentId::new),
operation_id: OperationId::new(row.operation_id),
status: deserialize_enum_text(&row.status, "status")?,
progress: row.progress_json,
result: row.result_json,
error: row.error_json,
expires_at: row.expires_at,
created_at: row.created_at,
updated_at: row.updated_at,
finished_at: row.finished_at,
})
})
.collect::<Result<Vec<_>, RegistryError>>()?;
let total = items.len() as u64;
Ok(Page { items, total })
+96 -24
View File
@@ -2,74 +2,118 @@ use super::*;
impl PostgresRegistry {
pub async fn list_workspaces(&self) -> Result<Vec<WorkspaceRecord>, RegistryError> {
let rows = sqlx::query(
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
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()
rows.into_iter()
.map(|row| {
Ok(WorkspaceRecord {
workspace: Workspace {
id: WorkspaceId::new(row.id),
slug: row.slug,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
settings: row.settings_json,
created_at: row.created_at,
updated_at: row.updated_at,
},
})
})
.collect()
}
pub async fn list_workspaces_for_user(
&self,
user_id: &UserId,
) -> Result<Vec<WorkspaceMembershipRecord>, RegistryError> {
let rows = sqlx::query(
let rows = sqlx::query!(
"select
w.id,
w.slug,
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,
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!\",
m.role
from memberships m
join workspaces w on w.id = m.workspace_id
where m.user_id = $1
order by w.slug asc",
user_id.as_str(),
)
.bind(user_id.as_str())
.fetch_all(&self.pool)
.await?;
rows.iter().map(map_workspace_membership_record).collect()
rows.into_iter()
.map(|row| {
Ok(WorkspaceMembershipRecord {
workspace: Workspace {
id: WorkspaceId::new(row.id),
slug: row.slug,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
settings: row.settings_json,
created_at: row.created_at,
updated_at: row.updated_at,
},
role: deserialize_enum_text(&row.role, "role")?,
})
})
.collect()
}
pub async fn list_memberships(
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<MembershipRecord>, RegistryError> {
let rows = sqlx::query(
let rows = sqlx::query!(
"select
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,
to_char(m.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"created_at!\",
u.email,
u.display_name,
u.status,
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as user_created_at
to_char(u.created_at at time zone 'UTC', 'YYYY-MM-DD\"T\"HH24:MI:SS\"Z\"') as \"user_created_at!\"
from memberships m
join users u on u.id = m.user_id
where m.workspace_id = $1
order by u.email asc",
workspace_id.as_str(),
)
.bind(workspace_id.as_str())
.fetch_all(&self.pool)
.await?;
rows.iter().map(map_membership_record).collect()
rows.into_iter()
.map(|row| {
Ok(MembershipRecord {
workspace_id: WorkspaceId::new(row.workspace_id),
user: User {
id: UserId::new(row.user_id),
email: row.email,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
created_at: row.user_created_at,
},
role: deserialize_enum_text(&row.role, "role")?,
created_at: row.created_at,
})
})
.collect()
}
pub async fn update_membership_role(
@@ -127,7 +171,7 @@ impl PostgresRegistry {
&self,
workspace_id: &WorkspaceId,
) -> Result<Vec<InvitationRecord>, RegistryError> {
let rows = sqlx::query(
let rows = sqlx::query!(
"select
id,
workspace_id,
@@ -135,17 +179,32 @@ 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
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!\"
from invitation_tokens
where workspace_id = $1
order by created_at desc",
workspace_id.as_str(),
)
.bind(workspace_id.as_str())
.fetch_all(&self.pool)
.await?;
rows.iter().map(map_invitation_record).collect()
rows.into_iter()
.map(|row| {
Ok(InvitationRecord {
invitation: InvitationToken {
id: InvitationId::new(row.id),
workspace_id: WorkspaceId::new(row.workspace_id),
email: row.email,
role: deserialize_enum_text(&row.role, "role")?,
status: deserialize_enum_text(&row.status, "status")?,
token_hash: row.token_hash,
expires_at: row.expires_at,
created_at: row.created_at,
},
})
})
.collect()
}
pub async fn create_invitation(
@@ -220,23 +279,36 @@ impl PostgresRegistry {
&self,
workspace_id: &WorkspaceId,
) -> Result<Option<WorkspaceRecord>, RegistryError> {
let row = sqlx::query(
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
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",
workspace_id.as_str(),
)
.bind(workspace_id.as_str())
.fetch_optional(&self.pool)
.await?;
row.as_ref().map(map_workspace_record).transpose()
row.map(|row| {
Ok(WorkspaceRecord {
workspace: Workspace {
id: WorkspaceId::new(row.id),
slug: row.slug,
display_name: row.display_name,
status: deserialize_enum_text(&row.status, "status")?,
settings: row.settings_json,
created_at: row.created_at,
updated_at: row.updated_at,
},
})
})
.transpose()
}
pub async fn create_workspace(