refactor: move ws_specific from resource column to separate table (#8766)
* Move ws_specific to separate table * on delete cascade * feat: handle ws_specific on resource rename and delete Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * is_false never used --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
16
backend/.sqlx/query-20ca0b26a0d2e9cfb9fb8e7066d6d5f3a154de0f76ce0d12ee678bcabd7b445c.json
generated
Normal file
16
backend/.sqlx/query-20ca0b26a0d2e9cfb9fb8e7066d6d5f3a154de0f76ce0d12ee678bcabd7b445c.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE ws_specific SET path = $1 WHERE workspace_id = $2 AND item_kind = 'resource' AND path = $3",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "20ca0b26a0d2e9cfb9fb8e7066d6d5f3a154de0f76ce0d12ee678bcabd7b445c"
|
||||
}
|
||||
15
backend/.sqlx/query-2d236ba78748c6160abec9ae1ad11e9e90063e2c583e2145f6d5b86fbf981861.json
generated
Normal file
15
backend/.sqlx/query-2d236ba78748c6160abec9ae1ad11e9e90063e2c583e2145f6d5b86fbf981861.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "2d236ba78748c6160abec9ae1ad11e9e90063e2c583e2145f6d5b86fbf981861"
|
||||
}
|
||||
15
backend/.sqlx/query-3ea2e1994d576e7a1b32ab276820dcff8e4e3b83f0d6a78fa0120c73fe90f345.json
generated
Normal file
15
backend/.sqlx/query-3ea2e1994d576e7a1b32ab276820dcff8e4e3b83f0d6a78fa0120c73fe90f345.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "DELETE FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = ANY($2)",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "3ea2e1994d576e7a1b32ab276820dcff8e4e3b83f0d6a78fa0120c73fe90f345"
|
||||
}
|
||||
@@ -50,36 +50,31 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "ws_specific",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"name": "is_expired",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 10,
|
||||
"name": "is_refreshed",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 11,
|
||||
"name": "refresh_error",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 12,
|
||||
"name": "is_linked",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 13,
|
||||
"name": "is_oauth?",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 14,
|
||||
"name": "account",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
@@ -100,7 +95,6 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
null,
|
||||
null,
|
||||
true,
|
||||
|
||||
@@ -47,11 +47,6 @@
|
||||
"ordinal": 8,
|
||||
"name": "labels",
|
||||
"type_info": "TextArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"name": "ws_specific",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
@@ -68,8 +63,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "45e4d13f5806122faecdb1d9ab18159555b652869a036b006f4a151e999b17b7"
|
||||
|
||||
23
backend/.sqlx/query-4a623e8789073abd92c52615077b89ed24398f59205c64290fa52ab19b3e44a3.json
generated
Normal file
23
backend/.sqlx/query-4a623e8789073abd92c52615077b89ed24398f59205c64290fa52ab19b3e44a3.json
generated
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT EXISTS(SELECT 1 FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = $2)",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "exists",
|
||||
"type_info": "Bool"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4a623e8789073abd92c52615077b89ed24398f59205c64290fa52ab19b3e44a3"
|
||||
}
|
||||
35
backend/.sqlx/query-819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33.json
generated
Normal file
35
backend/.sqlx/query-819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33.json
generated
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "SELECT value, description, resource_type\n FROM resource\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "value",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "description",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "resource_type",
|
||||
"type_info": "Varchar"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
true,
|
||||
true,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "819c233915383e89af1bcf1a56c5f67c4e1fc217f216f609e36a9944a7807b33"
|
||||
}
|
||||
16
backend/.sqlx/query-c19df45b092a246141e52770e70e02293b0a5eac859cbdc45f07c08957f00e00.json
generated
Normal file
16
backend/.sqlx/query-c19df45b092a246141e52770e70e02293b0a5eac859cbdc45f07c08957f00e00.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "UPDATE resource\n SET value = jsonb_set(value, '{dbname}', to_jsonb($3::text))\n WHERE workspace_id = $1 AND path = $2",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c19df45b092a246141e52770e70e02293b0a5eac859cbdc45f07c08957f00e00"
|
||||
}
|
||||
20
backend/.sqlx/query-deac41298e8b0d0870e314fef0813c24dd55d63bda78a0a5f35ed6f22bea6bef.json
generated
Normal file
20
backend/.sqlx/query-deac41298e8b0d0870e314fef0813c24dd55d63bda78a0a5f35ed6f22bea6bef.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO resource\n (workspace_id, path, value, description, resource_type, created_by, edited_at, labels)\n VALUES ($1, $2, $3, $4, $5, $6, now(), $7) ON CONFLICT (workspace_id, path)\n DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now(), labels = EXCLUDED.labels",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Jsonb",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "deac41298e8b0d0870e314fef0813c24dd55d63bda78a0a5f35ed6f22bea6bef"
|
||||
}
|
||||
15
backend/.sqlx/query-edcda7e58fd1c586b5fb7aadedfde6b9dcffbc859426295c58719f6a8a544936.json
generated
Normal file
15
backend/.sqlx/query-edcda7e58fd1c586b5fb7aadedfde6b9dcffbc859426295c58719f6a8a544936.json
generated
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "INSERT INTO ws_specific (workspace_id, item_kind, path) VALUES ($1, 'resource', $2) ON CONFLICT DO NOTHING",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "edcda7e58fd1c586b5fb7aadedfde6b9dcffbc859426295c58719f6a8a544936"
|
||||
}
|
||||
12
backend/migrations/20260408151657_ws_specific_table.down.sql
Normal file
12
backend/migrations/20260408151657_ws_specific_table.down.sql
Normal file
@@ -0,0 +1,12 @@
|
||||
-- Re-add the column to resource
|
||||
ALTER TABLE resource ADD COLUMN IF NOT EXISTS ws_specific BOOLEAN NOT NULL DEFAULT false;
|
||||
|
||||
-- Migrate data back
|
||||
UPDATE resource SET ws_specific = true
|
||||
FROM ws_specific ws
|
||||
WHERE ws.workspace_id = resource.workspace_id
|
||||
AND ws.item_kind = 'resource'
|
||||
AND ws.path = resource.path;
|
||||
|
||||
-- Drop the table
|
||||
DROP TABLE IF EXISTS ws_specific;
|
||||
17
backend/migrations/20260408151657_ws_specific_table.up.sql
Normal file
17
backend/migrations/20260408151657_ws_specific_table.up.sql
Normal file
@@ -0,0 +1,17 @@
|
||||
-- Create the ws_specific table
|
||||
CREATE TABLE IF NOT EXISTS ws_specific (
|
||||
workspace_id VARCHAR(50) NOT NULL REFERENCES workspace(id) ON DELETE CASCADE ON UPDATE CASCADE,
|
||||
item_kind VARCHAR(50) NOT NULL,
|
||||
path VARCHAR(255) NOT NULL,
|
||||
PRIMARY KEY (workspace_id, item_kind, path)
|
||||
);
|
||||
|
||||
-- Migrate existing data from resource.ws_specific
|
||||
INSERT INTO ws_specific (workspace_id, item_kind, path)
|
||||
SELECT workspace_id, 'resource', path
|
||||
FROM resource
|
||||
WHERE ws_specific = true
|
||||
ON CONFLICT DO NOTHING;
|
||||
|
||||
-- Drop the column from resource
|
||||
ALTER TABLE resource DROP COLUMN IF EXISTS ws_specific;
|
||||
@@ -4160,7 +4160,7 @@ async fn create_workspace_fork_branch(
|
||||
|
||||
/// Update a forked workspace's datatable config to point to the new database.
|
||||
/// For instance datatables: updates resource_path in the datatable config.
|
||||
/// For resource datatables: updates the resource's dbname and sets ws_specific.
|
||||
/// For resource datatables: updates the resource's dbname and marks it as ws_specific.
|
||||
/// Snapshot the schema from the source datatable by connecting to its database.
|
||||
async fn snapshot_datatable_schema(
|
||||
db: &DB,
|
||||
@@ -4242,12 +4242,11 @@ async fn apply_forked_datatable(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
} else {
|
||||
// Resource: update the resource's dbname and set ws_specific
|
||||
// Resource: update the resource's dbname and mark as ws_specific
|
||||
let resource_path = &dt.database.resource_path;
|
||||
sqlx::query!(
|
||||
r#"UPDATE resource
|
||||
SET value = jsonb_set(value, '{dbname}', to_jsonb($3::text)),
|
||||
ws_specific = true
|
||||
SET value = jsonb_set(value, '{dbname}', to_jsonb($3::text))
|
||||
WHERE workspace_id = $1 AND path = $2"#,
|
||||
forked_w_id,
|
||||
resource_path,
|
||||
@@ -4256,6 +4255,14 @@ async fn apply_forked_datatable(
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"INSERT INTO ws_specific (workspace_id, item_kind, path) VALUES ($1, 'resource', $2) ON CONFLICT DO NOTHING",
|
||||
forked_w_id,
|
||||
resource_path,
|
||||
)
|
||||
.execute(&mut **tx)
|
||||
.await?;
|
||||
|
||||
// Set forked_from on the datatable config
|
||||
sqlx::query!(
|
||||
r#"UPDATE workspace_settings
|
||||
@@ -6081,7 +6088,7 @@ async fn compare_two_resources(
|
||||
) -> Result<ItemComparison> {
|
||||
// Get resource from each workspace
|
||||
let source_resource = sqlx::query!(
|
||||
"SELECT value, description, resource_type, ws_specific
|
||||
"SELECT value, description, resource_type
|
||||
FROM resource
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
source_workspace_id,
|
||||
@@ -6091,7 +6098,7 @@ async fn compare_two_resources(
|
||||
.await?;
|
||||
|
||||
let target_resource = sqlx::query!(
|
||||
"SELECT value, description, resource_type, ws_specific
|
||||
"SELECT value, description, resource_type
|
||||
FROM resource
|
||||
WHERE workspace_id = $1 AND path = $2",
|
||||
fork_workspace_id,
|
||||
@@ -6101,8 +6108,24 @@ async fn compare_two_resources(
|
||||
.await?;
|
||||
|
||||
// If either side is ws_specific, consider unchanged
|
||||
let source_ws_specific = source_resource.as_ref().map_or(false, |r| r.ws_specific);
|
||||
let target_ws_specific = target_resource.as_ref().map_or(false, |r| r.ws_specific);
|
||||
let source_ws_specific = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = $2)",
|
||||
source_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
let target_ws_specific = sqlx::query_scalar!(
|
||||
"SELECT EXISTS(SELECT 1 FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = $2)",
|
||||
fork_workspace_id,
|
||||
path
|
||||
)
|
||||
.fetch_one(db)
|
||||
.await?
|
||||
.unwrap_or(false);
|
||||
|
||||
if source_ws_specific || target_ws_specific {
|
||||
return Ok(ItemComparison {
|
||||
has_changes: false,
|
||||
|
||||
@@ -21659,9 +21659,6 @@ components:
|
||||
resource_type:
|
||||
type: string
|
||||
description: The new resource_type to be associated with the resource
|
||||
ws_specific:
|
||||
type: boolean
|
||||
description: When true, the resource is excluded from workspace diff comparisons
|
||||
labels:
|
||||
type: array
|
||||
items:
|
||||
|
||||
@@ -87,10 +87,6 @@ pub fn public_service() -> Router {
|
||||
Router::new().route("/custom_component/{name}", get(custom_component))
|
||||
}
|
||||
|
||||
fn is_false(b: &bool) -> bool {
|
||||
!b
|
||||
}
|
||||
|
||||
#[derive(FromRow, Serialize, Deserialize)]
|
||||
pub struct ResourceType {
|
||||
pub workspace_id: String,
|
||||
@@ -129,8 +125,6 @@ pub struct Resource {
|
||||
pub extra_perms: serde_json::Value,
|
||||
pub created_by: Option<String>,
|
||||
pub edited_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub ws_specific: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<Vec<String>>,
|
||||
}
|
||||
@@ -151,8 +145,6 @@ pub struct ListableResource {
|
||||
pub is_expired: Option<bool>,
|
||||
pub refresh_error: Option<String>,
|
||||
pub account: Option<i32>,
|
||||
#[serde(default, skip_serializing_if = "is_false")]
|
||||
pub ws_specific: bool,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub labels: Option<Vec<String>>,
|
||||
}
|
||||
@@ -163,8 +155,6 @@ pub struct CreateResource {
|
||||
pub value: Option<Box<RawValue>>,
|
||||
pub description: Option<String>,
|
||||
pub resource_type: String,
|
||||
#[serde(default)]
|
||||
pub ws_specific: Option<bool>,
|
||||
pub labels: Option<Vec<String>>,
|
||||
}
|
||||
#[derive(Deserialize)]
|
||||
@@ -172,7 +162,6 @@ struct EditResource {
|
||||
path: Option<String>,
|
||||
description: Option<String>,
|
||||
value: Option<Box<RawValue>>,
|
||||
ws_specific: Option<bool>,
|
||||
labels: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -270,7 +259,6 @@ async fn list_resources(
|
||||
"account.refresh_error",
|
||||
"resource.created_by",
|
||||
"resource.edited_at",
|
||||
"resource.ws_specific",
|
||||
"resource.labels",
|
||||
])
|
||||
.left()
|
||||
@@ -847,16 +835,15 @@ async fn create_resource(
|
||||
}
|
||||
sqlx::query!(
|
||||
"INSERT INTO resource
|
||||
(workspace_id, path, value, description, resource_type, created_by, edited_at, ws_specific, labels)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, now(), $7, $8) ON CONFLICT (workspace_id, path)
|
||||
DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now(), ws_specific = EXCLUDED.ws_specific, labels = EXCLUDED.labels",
|
||||
(workspace_id, path, value, description, resource_type, created_by, edited_at, labels)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, now(), $7) ON CONFLICT (workspace_id, path)
|
||||
DO UPDATE SET value = EXCLUDED.value, description = EXCLUDED.description, resource_type = EXCLUDED.resource_type, edited_at = now(), labels = EXCLUDED.labels",
|
||||
w_id,
|
||||
resource.path,
|
||||
raw_json as sqlx::types::Json<&RawValue>,
|
||||
resource.description,
|
||||
resource.resource_type,
|
||||
authed.username,
|
||||
resource.ws_specific.unwrap_or(false),
|
||||
resource.labels.as_deref() as Option<&[String]>
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
@@ -963,6 +950,14 @@ async fn delete_resource(
|
||||
q.fetch_all(&mut *tx).await?
|
||||
};
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = $2",
|
||||
w_id,
|
||||
path
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let deleted_path = sqlx::query_scalar!(
|
||||
"DELETE FROM resource WHERE path = $1 AND workspace_id = $2 RETURNING path",
|
||||
path,
|
||||
@@ -1138,6 +1133,14 @@ async fn delete_resources_bulk(
|
||||
}
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"DELETE FROM ws_specific WHERE workspace_id = $1 AND item_kind = 'resource' AND path = ANY($2)",
|
||||
w_id,
|
||||
&request.paths
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
let deleted_paths = sqlx::query_scalar!(
|
||||
"DELETE FROM resource WHERE path = ANY($1) AND workspace_id = $2 RETURNING path",
|
||||
&request.paths,
|
||||
@@ -1224,10 +1227,6 @@ async fn update_resource(
|
||||
if let Some(ndesc) = ns.description {
|
||||
sqlb.set_str("description", ndesc);
|
||||
}
|
||||
if let Some(nd) = ns.ws_specific {
|
||||
sqlb.set_str("ws_specific", if nd { "true" } else { "false" });
|
||||
}
|
||||
|
||||
sqlb.set_str("edited_at", "now()");
|
||||
|
||||
sqlb.returning("path");
|
||||
@@ -1286,6 +1285,15 @@ async fn update_resource(
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"UPDATE ws_specific SET path = $1 WHERE workspace_id = $2 AND item_kind = 'resource' AND path = $3",
|
||||
npath,
|
||||
w_id,
|
||||
path
|
||||
)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ export interface ResourceFile {
|
||||
description?: string;
|
||||
resource_type: string;
|
||||
is_oauth?: boolean; // deprecated
|
||||
ws_specific?: boolean;
|
||||
}
|
||||
|
||||
async function readFilesetDirectory(dirPath: string): Promise<Record<string, string>> {
|
||||
|
||||
@@ -418,7 +418,6 @@ impl Windmill {
|
||||
description: None,
|
||||
value: Some(value),
|
||||
resource_type: Some(resource_type.to_owned()),
|
||||
ws_specific: None,
|
||||
labels: None,
|
||||
},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user