diff --git a/Cargo.lock b/Cargo.lock index 05283b76..d9c20b62 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3231,7 +3231,7 @@ checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" [[package]] name = "registry_api" -version = "0.1.0" +version = "1.1.0" dependencies = [ "ammonia", "anyhow", diff --git a/api/.sqlx/query-49430df890473cb2dd99a3461ec495a4b51eac2aa95df8b9d2f057668e704e79.json b/api/.sqlx/query-0ddb2cf66ee7e7d2c7341713b39b8dfe51e9edc309c346baaea94470cace57af.json similarity index 70% rename from api/.sqlx/query-49430df890473cb2dd99a3461ec495a4b51eac2aa95df8b9d2f057668e704e79.json rename to api/.sqlx/query-0ddb2cf66ee7e7d2c7341713b39b8dfe51e9edc309c346baaea94470cace57af.json index aa14e290..90254427 100644 --- a/api/.sqlx/query-49430df890473cb2dd99a3461ec495a4b51eac2aa95df8b9d2f057668e704e79.json +++ b/api/.sqlx/query-0ddb2cf66ee7e7d2c7341713b39b8dfe51e9edc309c346baaea94470cace57af.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n scope as \"scope: ScopeName\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM scopes WHERE creator = $1\n ORDER BY scope ASC", + "query": "SELECT\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM scopes WHERE creator = $1\n ORDER BY scope ASC", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -63,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "49430df890473cb2dd99a3461ec495a4b51eac2aa95df8b9d2f057668e704e79" + "hash": "0ddb2cf66ee7e7d2c7341713b39b8dfe51e9edc309c346baaea94470cace57af" } diff --git a/api/.sqlx/query-0b1bcc717b88e251ca9bcfc5ba771c44d369a7fc3a49d83a99116a36644331f8.json b/api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json similarity index 68% rename from api/.sqlx/query-0b1bcc717b88e251ca9bcfc5ba771c44d369a7fc3a49d83a99116a36644331f8.json rename to api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json index d426e064..2d3ecd6d 100644 --- a/api/.sqlx/query-0b1bcc717b88e251ca9bcfc5ba771c44d369a7fc3a49d83a99116a36644331f8.json +++ b/api/.sqlx/query-103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH usage AS (\n SELECT\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1) AS package,\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1 AND created_at > now() - '1 week'::interval) AS new_package_per_week,\n (SELECT COUNT(created_at) FROM publishing_tasks WHERE package_scope = $1 AND created_at > now() - '1 week'::interval) AS publish_attempts_per_week\n )\n SELECT\n scopes.scope as \"scope_scope: ScopeName\",\n scopes.creator as \"scope_creator\",\n scopes.package_limit as \"scope_package_limit\",\n scopes.new_package_per_week_limit as \"scope_new_package_per_week_limit\",\n scopes.publish_attempts_per_week_limit as \"scope_publish_attempts_per_week_limit\",\n scopes.verify_oidc_actor as \"scope_verify_oidc_actor\",\n scopes.require_publishing_from_ci as \"scope_require_publishing_from_ci\",\n scopes.updated_at as \"scope_updated_at\",\n scopes.created_at as \"scope_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\",\n usage.package as \"usage_package\", usage.new_package_per_week as \"usage_new_package_per_week\", usage.publish_attempts_per_week as \"usage_publish_attempts_per_week\"\n FROM scopes\n LEFT JOIN users ON scopes.creator = users.id\n CROSS JOIN usage\n WHERE scopes.scope = $1\n ", + "query": "\n WITH usage AS (\n SELECT\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1) AS package,\n (SELECT COUNT(created_at) FROM packages WHERE scope = $1 AND created_at > now() - '1 week'::interval) AS new_package_per_week,\n (SELECT COUNT(created_at) FROM publishing_tasks WHERE package_scope = $1 AND created_at > now() - '1 week'::interval) AS publish_attempts_per_week\n )\n SELECT\n scopes.scope as \"scope_scope: ScopeName\",\n scopes.description as \"scope_description: ScopeDescription\",\n scopes.creator as \"scope_creator\",\n scopes.package_limit as \"scope_package_limit\",\n scopes.new_package_per_week_limit as \"scope_new_package_per_week_limit\",\n scopes.publish_attempts_per_week_limit as \"scope_publish_attempts_per_week_limit\",\n scopes.verify_oidc_actor as \"scope_verify_oidc_actor\",\n scopes.require_publishing_from_ci as \"scope_require_publishing_from_ci\",\n scopes.updated_at as \"scope_updated_at\",\n scopes.created_at as \"scope_created_at\",\n users.id as \"user_id\", users.name as \"user_name\", users.avatar_url as \"user_avatar_url\", users.github_id as \"user_github_id\", users.updated_at as \"user_updated_at\", users.created_at as \"user_created_at\",\n usage.package as \"usage_package\", usage.new_package_per_week as \"usage_new_package_per_week\", usage.publish_attempts_per_week as \"usage_publish_attempts_per_week\"\n FROM scopes\n LEFT JOIN users ON scopes.creator = users.id\n CROSS JOIN usage\n WHERE scopes.scope = $1\n ", "describe": { "columns": [ { @@ -10,86 +10,91 @@ }, { "ordinal": 1, + "name": "scope_description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "scope_creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "scope_package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "scope_new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "scope_publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "scope_verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "scope_require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "scope_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "scope_created_at", "type_info": "Timestamptz" }, { - "ordinal": 9, + "ordinal": 10, "name": "user_id", "type_info": "Uuid" }, { - "ordinal": 10, + "ordinal": 11, "name": "user_name", "type_info": "Text" }, { - "ordinal": 11, + "ordinal": 12, "name": "user_avatar_url", "type_info": "Text" }, { - "ordinal": 12, + "ordinal": 13, "name": "user_github_id", "type_info": "Int8" }, { - "ordinal": 13, + "ordinal": 14, "name": "user_updated_at", "type_info": "Timestamptz" }, { - "ordinal": 14, + "ordinal": 15, "name": "user_created_at", "type_info": "Timestamptz" }, { - "ordinal": 15, + "ordinal": 16, "name": "usage_package", "type_info": "Int8" }, { - "ordinal": 16, + "ordinal": 17, "name": "usage_new_package_per_week", "type_info": "Int8" }, { - "ordinal": 17, + "ordinal": 18, "name": "usage_publish_attempts_per_week", "type_info": "Int8" } @@ -112,6 +117,7 @@ false, false, false, + false, true, false, false, @@ -120,5 +126,5 @@ null ] }, - "hash": "0b1bcc717b88e251ca9bcfc5ba771c44d369a7fc3a49d83a99116a36644331f8" + "hash": "103d72977104cf3702fcaa8ea92f1393155d5b4efa8c953c64b6a323a660c51b" } diff --git a/api/.sqlx/query-500d0cd93fa62096e30d12d646afd18f9880a4b033ea3bf79e989e9f6cb1260e.json b/api/.sqlx/query-17a9974ea1d81a38a5ab39b4ad95a70f27bdc3fd4e9334032d61ab85b72768a6.json similarity index 71% rename from api/.sqlx/query-500d0cd93fa62096e30d12d646afd18f9880a4b033ea3bf79e989e9f6cb1260e.json rename to api/.sqlx/query-17a9974ea1d81a38a5ab39b4ad95a70f27bdc3fd4e9334032d61ab85b72768a6.json index 47040b29..9df0ae8f 100644 --- a/api/.sqlx/query-500d0cd93fa62096e30d12d646afd18f9880a4b033ea3bf79e989e9f6cb1260e.json +++ b/api/.sqlx/query-17a9974ea1d81a38a5ab39b4ad95a70f27bdc3fd4e9334032d61ab85b72768a6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n scope as \"scope: ScopeName\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM scopes WHERE scope = $1", + "query": "SELECT\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM scopes WHERE scope = $1", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -63,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "500d0cd93fa62096e30d12d646afd18f9880a4b033ea3bf79e989e9f6cb1260e" + "hash": "17a9974ea1d81a38a5ab39b4ad95a70f27bdc3fd4e9334032d61ab85b72768a6" } diff --git a/api/.sqlx/query-133923c61b4021d34fb89119a11e045f29210264531695165d20e0e978a37069.json b/api/.sqlx/query-a0cf6f765477777ce12e6f05fbcc0d20546a69599a901080e8d1d7ffc476957b.json similarity index 56% rename from api/.sqlx/query-133923c61b4021d34fb89119a11e045f29210264531695165d20e0e978a37069.json rename to api/.sqlx/query-a0cf6f765477777ce12e6f05fbcc0d20546a69599a901080e8d1d7ffc476957b.json index 6a721201..6f0bffdc 100644 --- a/api/.sqlx/query-133923c61b4021d34fb89119a11e045f29210264531695165d20e0e978a37069.json +++ b/api/.sqlx/query-a0cf6f765477777ce12e6f05fbcc0d20546a69599a901080e8d1d7ffc476957b.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n WITH ins_scope AS (\n INSERT INTO scopes (scope, creator) VALUES ($1, $2)\n RETURNING\n scope,\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n ),\n ins_member AS (\n INSERT INTO scope_members (scope, user_id, is_admin)\n VALUES ($1, $2, true)\n )\n SELECT\n scope as \"scope: ScopeName\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM ins_scope\n ", + "query": "\n WITH ins_scope AS (\n INSERT INTO scopes (scope, creator) VALUES ($1, $2)\n RETURNING\n scope,\n description,\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n ),\n ins_member AS (\n INSERT INTO scope_members (scope, user_id, is_admin)\n VALUES ($1, $2, true)\n )\n SELECT\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n FROM ins_scope\n ", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -64,8 +69,9 @@ false, false, false, + false, false ] }, - "hash": "133923c61b4021d34fb89119a11e045f29210264531695165d20e0e978a37069" + "hash": "a0cf6f765477777ce12e6f05fbcc0d20546a69599a901080e8d1d7ffc476957b" } diff --git a/api/.sqlx/query-a83536c60d9f16008602be1f0111dfd4b84fd990ac8f1f558057212427383aab.json b/api/.sqlx/query-a83536c60d9f16008602be1f0111dfd4b84fd990ac8f1f558057212427383aab.json new file mode 100644 index 00000000..d60a587b --- /dev/null +++ b/api/.sqlx/query-a83536c60d9f16008602be1f0111dfd4b84fd990ac8f1f558057212427383aab.json @@ -0,0 +1,77 @@ +{ + "db_name": "PostgreSQL", + "query": "\n UPDATE scopes SET description = $1 WHERE scope = $2\n RETURNING\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "scope: ScopeName", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "creator", + "type_info": "Uuid" + }, + { + "ordinal": 3, + "name": "package_limit", + "type_info": "Int4" + }, + { + "ordinal": 4, + "name": "new_package_per_week_limit", + "type_info": "Int4" + }, + { + "ordinal": 5, + "name": "publish_attempts_per_week_limit", + "type_info": "Int4" + }, + { + "ordinal": 6, + "name": "verify_oidc_actor", + "type_info": "Bool" + }, + { + "ordinal": 7, + "name": "require_publishing_from_ci", + "type_info": "Bool" + }, + { + "ordinal": 8, + "name": "updated_at", + "type_info": "Timestamptz" + }, + { + "ordinal": 9, + "name": "created_at", + "type_info": "Timestamptz" + } + ], + "parameters": { + "Left": [ + "Text", + "Text" + ] + }, + "nullable": [ + false, + false, + false, + false, + false, + false, + false, + false, + false, + false + ] + }, + "hash": "a83536c60d9f16008602be1f0111dfd4b84fd990ac8f1f558057212427383aab" +} diff --git a/api/.sqlx/query-24d150e096a117abbeabc5f42029ad38eee8d37928c557141d6526f8bf5ce320.json b/api/.sqlx/query-d88d885e4a737dbe4602a46b83cf0ce70d75216e6252b4ceebc75c0c6d542afb.json similarity index 71% rename from api/.sqlx/query-24d150e096a117abbeabc5f42029ad38eee8d37928c557141d6526f8bf5ce320.json rename to api/.sqlx/query-d88d885e4a737dbe4602a46b83cf0ce70d75216e6252b4ceebc75c0c6d542afb.json index 096b8ac9..1e53e862 100644 --- a/api/.sqlx/query-24d150e096a117abbeabc5f42029ad38eee8d37928c557141d6526f8bf5ce320.json +++ b/api/.sqlx/query-d88d885e4a737dbe4602a46b83cf0ce70d75216e6252b4ceebc75c0c6d542afb.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE scopes SET require_publishing_from_ci = $1 WHERE scope = $2\n RETURNING\n scope as \"scope: ScopeName\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n\n ", + "query": "\n UPDATE scopes SET require_publishing_from_ci = $1 WHERE scope = $2\n RETURNING\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n\n ", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -64,8 +69,9 @@ false, false, false, + false, false ] }, - "hash": "24d150e096a117abbeabc5f42029ad38eee8d37928c557141d6526f8bf5ce320" + "hash": "d88d885e4a737dbe4602a46b83cf0ce70d75216e6252b4ceebc75c0c6d542afb" } diff --git a/api/.sqlx/query-d8211a55290446499c4a2de2defbeaad13728e8811eee2befea96524c2367a1d.json b/api/.sqlx/query-e5ed4e2eecd3d05da017201d5450aef03f54e1b6984b94c97cd337389be406f3.json similarity index 72% rename from api/.sqlx/query-d8211a55290446499c4a2de2defbeaad13728e8811eee2befea96524c2367a1d.json rename to api/.sqlx/query-e5ed4e2eecd3d05da017201d5450aef03f54e1b6984b94c97cd337389be406f3.json index db9899d1..1e8c2f3e 100644 --- a/api/.sqlx/query-d8211a55290446499c4a2de2defbeaad13728e8811eee2befea96524c2367a1d.json +++ b/api/.sqlx/query-e5ed4e2eecd3d05da017201d5450aef03f54e1b6984b94c97cd337389be406f3.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n UPDATE scopes SET verify_oidc_actor = $1 WHERE scope = $2\n RETURNING\n scope as \"scope: ScopeName\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n\n ", + "query": "\n UPDATE scopes SET verify_oidc_actor = $1 WHERE scope = $2\n RETURNING\n scope as \"scope: ScopeName\",\n description as \"description: ScopeDescription\",\n creator,\n package_limit,\n new_package_per_week_limit,\n publish_attempts_per_week_limit,\n verify_oidc_actor,\n require_publishing_from_ci,\n updated_at,\n created_at\n\n ", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -64,8 +69,9 @@ false, false, false, + false, false ] }, - "hash": "d8211a55290446499c4a2de2defbeaad13728e8811eee2befea96524c2367a1d" + "hash": "e5ed4e2eecd3d05da017201d5450aef03f54e1b6984b94c97cd337389be406f3" } diff --git a/api/.sqlx/query-353d93cde6d9eace115c2753931d4d706f9ac9c599db7ffa1e06ad6fc944faea.json b/api/.sqlx/query-fff5990a48eda487a16c71fb6454d5c2b983a82f4c5c5eccb92e1ecc1dfe063c.json similarity index 86% rename from api/.sqlx/query-353d93cde6d9eace115c2753931d4d706f9ac9c599db7ffa1e06ad6fc944faea.json rename to api/.sqlx/query-fff5990a48eda487a16c71fb6454d5c2b983a82f4c5c5eccb92e1ecc1dfe063c.json index 1e6de23a..077446c4 100644 --- a/api/.sqlx/query-353d93cde6d9eace115c2753931d4d706f9ac9c599db7ffa1e06ad6fc944faea.json +++ b/api/.sqlx/query-fff5990a48eda487a16c71fb6454d5c2b983a82f4c5c5eccb92e1ecc1dfe063c.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT\n scopes.scope as \"scope: ScopeName\",\n scopes.creator,\n scopes.package_limit,\n scopes.new_package_per_week_limit,\n scopes.publish_attempts_per_week_limit,\n scopes.verify_oidc_actor,\n scopes.require_publishing_from_ci,\n scopes.updated_at,\n scopes.created_at\n FROM scopes\n LEFT JOIN scope_members ON scope_members.scope = scopes.scope\n WHERE user_id = $1", + "query": "SELECT\n scopes.scope as \"scope: ScopeName\",\n scopes.description as \"description: ScopeDescription\",\n scopes.creator,\n scopes.package_limit,\n scopes.new_package_per_week_limit,\n scopes.publish_attempts_per_week_limit,\n scopes.verify_oidc_actor,\n scopes.require_publishing_from_ci,\n scopes.updated_at,\n scopes.created_at\n FROM scopes\n LEFT JOIN scope_members ON scope_members.scope = scopes.scope\n WHERE user_id = $1", "describe": { "columns": [ { @@ -10,41 +10,46 @@ }, { "ordinal": 1, + "name": "description: ScopeDescription", + "type_info": "Text" + }, + { + "ordinal": 2, "name": "creator", "type_info": "Uuid" }, { - "ordinal": 2, + "ordinal": 3, "name": "package_limit", "type_info": "Int4" }, { - "ordinal": 3, + "ordinal": 4, "name": "new_package_per_week_limit", "type_info": "Int4" }, { - "ordinal": 4, + "ordinal": 5, "name": "publish_attempts_per_week_limit", "type_info": "Int4" }, { - "ordinal": 5, + "ordinal": 6, "name": "verify_oidc_actor", "type_info": "Bool" }, { - "ordinal": 6, + "ordinal": 7, "name": "require_publishing_from_ci", "type_info": "Bool" }, { - "ordinal": 7, + "ordinal": 8, "name": "updated_at", "type_info": "Timestamptz" }, { - "ordinal": 8, + "ordinal": 9, "name": "created_at", "type_info": "Timestamptz" } @@ -63,8 +68,9 @@ false, false, false, + false, false ] }, - "hash": "353d93cde6d9eace115c2753931d4d706f9ac9c599db7ffa1e06ad6fc944faea" + "hash": "fff5990a48eda487a16c71fb6454d5c2b983a82f4c5c5eccb92e1ecc1dfe063c" } diff --git a/api/Cargo.toml b/api/Cargo.toml index d567d68e..5e828d7d 100644 --- a/api/Cargo.toml +++ b/api/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "registry_api" -version = "0.1.0" +version = "1.1.0" edition = "2021" resolver = "2" diff --git a/api/migrations/20250502114042_adding_description_to_scrope.sql b/api/migrations/20250502114042_adding_description_to_scrope.sql new file mode 100644 index 00000000..a6139dd1 --- /dev/null +++ b/api/migrations/20250502114042_adding_description_to_scrope.sql @@ -0,0 +1 @@ +ALTER TABLE scopes ADD COLUMN description TEXT NOT NULL DEFAULT ''; diff --git a/api/src/api.yml b/api/src/api.yml index f45ffd18..1b51c638 100644 --- a/api/src/api.yml +++ b/api/src/api.yml @@ -1,7 +1,7 @@ openapi: 3.0.1 info: title: jsr API - version: "1.0.0" + version: "1.1.0" servers: - url: https://api.jsr.io @@ -1560,6 +1560,10 @@ components: properties: scope: $ref: "#/components/schemas/ScopeName" + description: + type: string + description: The description of the scope. + required: - name diff --git a/api/src/api/admin.rs b/api/src/api/admin.rs index d34a0f4b..caf001ea 100644 --- a/api/src/api/admin.rs +++ b/api/src/api/admin.rs @@ -15,6 +15,7 @@ use tracing::Span; use crate::db::*; use crate::iam::ReqIamExt; +use crate::ids::ScopeDescription; use crate::publish::publish_task; use crate::util; use crate::util::decode_json; @@ -202,7 +203,13 @@ pub async fn assign_scope(mut req: Request) -> ApiResult { } let scope = db - .create_scope(&staff.id, true, &scope, user_id) + .create_scope( + &staff.id, + true, + &scope, + user_id, + &ScopeDescription::default(), + ) .await .map_err(|e| map_unique_violation(e, ApiError::ScopeAlreadyExists))?; diff --git a/api/src/api/package.rs b/api/src/api/package.rs index af59f9db..7aee6b27 100644 --- a/api/src/api/package.rs +++ b/api/src/api/package.rs @@ -2384,10 +2384,7 @@ mod test { use crate::db::Permissions; use crate::db::PublishingTaskStatus; use crate::db::TokenType; - use crate::ids::PackageName; - use crate::ids::PackagePath; - use crate::ids::ScopeName; - use crate::ids::Version; + use crate::ids::{PackageName, PackagePath, ScopeName, Version, ScopeDescription}; use crate::publish::tests::create_mock_tarball; use crate::publish::tests::process_tarball_setup; use crate::publish::tests::process_tarball_setup2; @@ -2585,7 +2582,13 @@ mod test { // create scope2 for user2, try creating a package with user1 let scope2 = ScopeName::new("scope2".into()).unwrap(); t.db() - .create_scope(&t.user2.user.id, false, &scope2, t.user2.user.id) + .create_scope( + &t.user2.user.id, + false, + &scope2, + t.user2.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); let mut resp = t diff --git a/api/src/api/scope.rs b/api/src/api/scope.rs index 9ecba9c2..4b150c0a 100644 --- a/api/src/api/scope.rs +++ b/api/src/api/scope.rs @@ -63,8 +63,10 @@ static RESERVED_SCOPES: OnceLock> = #[instrument(name = "POST /api/scopes", skip(req), err, fields(scope))] async fn create_handler(mut req: Request) -> ApiResult { - let ApiCreateScopeRequest { scope } = decode_json(&mut req).await?; + let ApiCreateScopeRequest { scope, description } = + decode_json(&mut req).await?; Span::current().record("scope", field::display(&scope)); + Span::current().record("description", field::display(&description)); let db = req.data::().unwrap(); @@ -94,7 +96,7 @@ async fn create_handler(mut req: Request) -> ApiResult { } let scope = db - .create_scope(&user.id, false, &scope, user.id) + .create_scope(&user.id, false, &scope, user.id, &description) .await .map_err(|e| map_unique_violation(e, ApiError::ScopeAlreadyExists))?; @@ -163,6 +165,11 @@ async fn update_handler( ) .await? } + ApiUpdateScopeRequest::Description(description) => { + let (user, sudo) = iam.check_scope_admin_access(&scope).await?; + db.scope_set_description(&user.id, sudo, &scope, description) + .await? + } }; let user = db @@ -493,8 +500,7 @@ pub async fn delete_invite_handler( #[cfg(test)] pub mod tests { use super::*; - use crate::ids::PackageName; - use crate::ids::ScopeName; + use crate::ids::{PackageName, ScopeDescription, ScopeName}; use crate::util::test::ApiResultExt; use crate::util::test::TestSetup; use serde_json::json; @@ -511,7 +517,9 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope1" })) + .body_json( + json!({ "scope": "scope1", "description": "" }), + ) .call() .await .unwrap(); @@ -526,7 +534,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope1" })) + .body_json(json!({ "scope": "scope1", "description": "" })) .call() .await .unwrap(); @@ -538,7 +546,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scop-e1" })) + .body_json(json!({ "scope": "scop-e1", "description": "" })) .call() .await .unwrap(); @@ -550,7 +558,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope 1" })) + .body_json(json!({ "scope": "scope 1", "description": "" })) .call() .await .unwrap(); @@ -561,7 +569,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "somebadword" })) + .body_json(json!({ "scope": "somebadword", "description": "" })) .call() .await .unwrap(); @@ -572,7 +580,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "react" })) + .body_json(json!({ "scope": "react", "description": "" })) .call() .await .unwrap(); @@ -598,7 +606,7 @@ pub mod tests { let mut resp = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope1" })) + .body_json(json!({ "scope": "scope1", "description": "Super scope 🐢 !!!" })) .call() .await .unwrap(); @@ -606,7 +614,7 @@ pub mod tests { let mut resp: Response = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope2" })) + .body_json(json!({ "scope": "scope2", "description": "Super scope 🐢 !!!" })) .call() .await .unwrap(); @@ -616,7 +624,7 @@ pub mod tests { let mut resp: Response = t .http() .post("/api/scopes") - .body_json(json!({ "scope": "scope3" })) + .body_json(json!({ "scope": "scope3", "description": "Another super scope 🐢 !!!" })) .call() .await .unwrap(); @@ -774,7 +782,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); let members = list_members(&mut t).await; @@ -838,7 +852,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -887,7 +907,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -936,7 +962,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -999,7 +1031,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1064,7 +1102,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1125,7 +1169,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1170,7 +1220,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1217,7 +1273,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1279,7 +1341,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1341,7 +1409,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1411,7 +1485,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1481,7 +1561,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1553,7 +1639,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1624,7 +1716,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1679,7 +1777,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1721,7 +1825,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1765,7 +1875,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1824,7 +1940,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1886,7 +2008,13 @@ pub mod tests { let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1969,7 +2097,13 @@ pub mod tests { // create scope let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -1990,7 +2124,13 @@ pub mod tests { // create scope let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); t.db() @@ -2031,7 +2171,13 @@ pub mod tests { // create scope let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); @@ -2068,7 +2214,13 @@ pub mod tests { // create scope and package let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); let name = PackageName::new("foo".to_owned()).unwrap(); @@ -2091,7 +2243,13 @@ pub mod tests { // create scope and package let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); t.db() @@ -2119,14 +2277,26 @@ pub mod tests { // create scope let scope_name = ScopeName::try_from("scope1").unwrap(); t.db() - .create_scope(&t.user1.user.id, false, &scope_name, t.user1.user.id) + .create_scope( + &t.user1.user.id, + false, + &scope_name, + t.user1.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); for i in 0..3 { let scope_name = ScopeName::try_from(format!("temp{i}")).unwrap(); t.db() - .create_scope(&t.user2.user.id, false, &scope_name, t.user2.user.id) + .create_scope( + &t.user2.user.id, + false, + &scope_name, + t.user2.user.id, + &ScopeDescription::default(), + ) .await .unwrap(); } diff --git a/api/src/api/types.rs b/api/src/api/types.rs index 4157f973..72f0c001 100644 --- a/api/src/api/types.rs +++ b/api/src/api/types.rs @@ -4,6 +4,7 @@ use std::borrow::Cow; use crate::db::*; use crate::ids::PackageName; use crate::ids::PackagePath; +use crate::ids::ScopeDescription; use crate::ids::ScopeName; use crate::ids::Version; use crate::provenance::ProvenanceBundle; @@ -192,6 +193,7 @@ impl From for ApiFullUser { #[serde(rename_all = "camelCase")] pub struct ApiScope { pub scope: ScopeName, + pub description: ScopeDescription, pub updated_at: DateTime, pub created_at: DateTime, } @@ -200,6 +202,7 @@ impl From for ApiScope { fn from(scope: Scope) -> Self { Self { scope: scope.scope, + description: scope.description, updated_at: scope.updated_at, created_at: scope.created_at, } @@ -221,6 +224,7 @@ pub struct ApiScopeQuotas { #[serde(rename_all = "camelCase")] pub struct ApiFullScope { pub scope: ScopeName, + pub description: ScopeDescription, pub creator: ApiUser, pub updated_at: DateTime, pub created_at: DateTime, @@ -235,6 +239,7 @@ impl From<(Scope, ScopeUsage, UserPublic)> for ApiFullScope { assert_eq!(scope.creator, user.id); Self { scope: scope.scope, + description: scope.description, creator: user.into(), updated_at: scope.updated_at, created_at: scope.created_at, @@ -263,6 +268,7 @@ pub enum ApiScopeOrFullScope { #[serde(rename_all = "camelCase")] pub struct ApiCreateScopeRequest { pub scope: ScopeName, + pub description: ScopeDescription, } #[derive(Debug, Serialize, Deserialize)] @@ -724,6 +730,8 @@ pub enum ApiUpdateScopeRequest { GhActionsVerifyActor(bool), #[serde(rename = "requirePublishingFromCI")] RequirePublishingFromCI(bool), + #[serde(rename = "description")] + Description(Option), } #[derive(Debug, Serialize, Deserialize)] diff --git a/api/src/db/database.rs b/api/src/db/database.rs index 87026fc3..3d208e4e 100644 --- a/api/src/db/database.rs +++ b/api/src/db/database.rs @@ -13,6 +13,7 @@ use uuid::Uuid; use crate::api::ApiMetrics; use crate::ids::PackageName; use crate::ids::PackagePath; +use crate::ids::ScopeDescription; use crate::ids::ScopeName; use crate::ids::Version; @@ -923,8 +924,9 @@ impl Database { &self, actor_id: &Uuid, is_sudo: bool, - scope: &ScopeName, + scope_name: &ScopeName, user_id: Uuid, + scope_description: &ScopeDescription, ) -> Result { let mut tx = self.pool.begin().await?; @@ -938,7 +940,7 @@ impl Database { "create_scope" }, json!({ - "scope": scope, + "scope": scope_name, "user_id": user_id, }), ) @@ -951,6 +953,7 @@ impl Database { INSERT INTO scopes (scope, creator) VALUES ($1, $2) RETURNING scope, + description, creator, package_limit, new_package_per_week_limit, @@ -966,6 +969,7 @@ impl Database { ) SELECT scope as "scope: ScopeName", + description as "description: ScopeDescription", creator, package_limit, new_package_per_week_limit, @@ -976,8 +980,8 @@ impl Database { created_at FROM ins_scope "#, - scope as _, - user_id + scope_name, + user_id, ) .fetch_one(&mut *tx) .await?; @@ -1076,6 +1080,7 @@ impl Database { ) SELECT scopes.scope as "scope_scope: ScopeName", + scopes.description as "scope_description: ScopeDescription", scopes.creator as "scope_creator", scopes.package_limit as "scope_package_limit", scopes.new_package_per_week_limit as "scope_new_package_per_week_limit", @@ -1096,6 +1101,7 @@ impl Database { .map(|r| { let scope = Scope { scope: r.scope_scope, + description: r.scope_description, creator: r.scope_creator, updated_at: r.scope_updated_at, created_at: r.scope_created_at, @@ -1153,6 +1159,7 @@ impl Database { let scopes = sqlx::query(&format!( r#"SELECT scopes.scope as "scope_scope", + scopes.description as "scope_description", scopes.creator as "scope_creator", scopes.package_limit as "scope_package_limit", scopes.new_package_per_week_limit as "scope_new_package_per_week_limit", @@ -1207,6 +1214,7 @@ impl Database { Scope, r#"SELECT scope as "scope: ScopeName", + description as "description: ScopeDescription", creator, package_limit, new_package_per_week_limit, @@ -1229,6 +1237,7 @@ impl Database { Scope, r#"SELECT scope as "scope: ScopeName", + description as "description: ScopeDescription", creator, package_limit, new_package_per_week_limit, @@ -1238,7 +1247,7 @@ impl Database { updated_at, created_at FROM scopes WHERE scope = $1"#, - scope as _ + scope ) .fetch_optional(&self.pool) .await @@ -1292,6 +1301,7 @@ impl Database { UPDATE scopes SET verify_oidc_actor = $1 WHERE scope = $2 RETURNING scope as "scope: ScopeName", + description as "description: ScopeDescription", creator, package_limit, new_package_per_week_limit, @@ -1345,6 +1355,7 @@ impl Database { UPDATE scopes SET require_publishing_from_ci = $1 WHERE scope = $2 RETURNING scope as "scope: ScopeName", + description as "description: ScopeDescription", creator, package_limit, new_package_per_week_limit, @@ -1366,6 +1377,56 @@ impl Database { Ok(scope) } + #[instrument(name = "Database::scope_set_description", skip(self), err)] + pub async fn scope_set_description( + &self, + actor_id: &Uuid, + is_sudo: bool, + scope: &ScopeName, + description: Option, + ) -> Result { + let mut tx = self.pool.begin().await?; + + audit_log( + &mut tx, + actor_id, + is_sudo, + "scope_set_description", + json!({ + "scope": scope, + "description": description, + }), + ) + .await?; + + let scope = sqlx::query_as!( + Scope, + r#" + UPDATE scopes SET description = $1 WHERE scope = $2 + RETURNING + scope as "scope: ScopeName", + description as "description: ScopeDescription", + creator, + package_limit, + new_package_per_week_limit, + publish_attempts_per_week_limit, + verify_oidc_actor, + require_publishing_from_ci, + updated_at, + created_at + + "#, + description, + scope as _ + ) + .fetch_one(&mut *tx) + .await?; + + tx.commit().await?; + + Ok(scope) + } + #[instrument(name = "Database::list_packages_by_scope", skip(self), err)] pub async fn list_packages_by_scope( &self, @@ -2325,6 +2386,7 @@ impl Database { Scope, r#"SELECT scopes.scope as "scope: ScopeName", + scopes.description as "description: ScopeDescription", scopes.creator, scopes.package_limit, scopes.new_package_per_week_limit, diff --git a/api/src/db/models.rs b/api/src/db/models.rs index 6c722ff6..11603fe5 100644 --- a/api/src/db/models.rs +++ b/api/src/db/models.rs @@ -15,6 +15,7 @@ use uuid::Uuid; use crate::ids::PackageName; use crate::ids::PackagePath; +use crate::ids::ScopeDescription; use crate::ids::ScopeName; use crate::ids::Version; @@ -208,6 +209,7 @@ pub struct NewPublishingTask<'s> { #[derive(Debug)] pub struct Scope { pub scope: ScopeName, + pub description: ScopeDescription, pub creator: Uuid, pub updated_at: DateTime, pub created_at: DateTime, @@ -237,6 +239,7 @@ impl FromRow<'_, sqlx::postgres::PgRow> for Scope { fn from_row(row: &sqlx::postgres::PgRow) -> Result { Ok(Self { scope: try_get_row_or(row, "scope", "scope_scope")?, + description: try_get_row_or(row, "description", "scope_description")?, creator: try_get_row_or(row, "creator", "scope_creator")?, updated_at: try_get_row_or(row, "updated_at", "scope_updated_at")?, created_at: try_get_row_or(row, "created_at", "scope_created_at")?, diff --git a/api/src/db/tests.rs b/api/src/db/tests.rs index d679b265..ddf67640 100644 --- a/api/src/db/tests.rs +++ b/api/src/db/tests.rs @@ -2,6 +2,7 @@ use crate::db::*; use crate::ids::PackageName; use crate::ids::PackagePath; +use crate::ids::ScopeDescription; use crate::ids::ScopeName; use crate::ids::Version; use crate::npm::NPM_TARBALL_REVISION; @@ -19,7 +20,13 @@ async fn publishing_tasks() { let config_file = "/jsr.json".try_into().unwrap(); let _scope = db - .create_scope(&user_id, false, &scope_name, user_id) + .create_scope( + &user_id, + false, + &scope_name, + user_id, + &ScopeDescription::default(), + ) .await .unwrap(); let res = db.create_package(&scope_name, &package_name).await.unwrap(); @@ -217,9 +224,15 @@ async fn packages() { let scope_name = "scope".try_into().unwrap(); let package_name = "testpkg".try_into().unwrap(); - db.create_scope(&alice.id, false, &scope_name, alice.id) - .await - .unwrap(); + db.create_scope( + &alice.id, + false, + &scope_name, + alice.id, + &ScopeDescription::default(), + ) + .await + .unwrap(); let alice2 = db.get_user(alice.id).await.unwrap().unwrap(); assert_eq!(alice2.scope_usage, 1); @@ -285,9 +298,15 @@ async fn scope_members() { let scope_name = "scope".try_into().unwrap(); - db.create_scope(&bob.id, false, &scope_name, bob.id) - .await - .unwrap(); + db.create_scope( + &bob.id, + false, + &scope_name, + bob.id, + &ScopeDescription::default(), + ) + .await + .unwrap(); let scope = db .get_scope(&ScopeName::try_from("scope").unwrap()) @@ -350,7 +369,7 @@ async fn create_package_version_and_finalize_publishing_task() { .await .unwrap(); - db.create_scope(&bob.id, false, &scope, bob.id) + db.create_scope(&bob.id, false, &scope, bob.id, &ScopeDescription::default()) .await .unwrap(); @@ -461,9 +480,15 @@ async fn package_files() { let package_name = "testpkg".try_into().unwrap(); let version = "1.2.3".try_into().unwrap(); - db.create_scope(&user.id, false, &scope_name, user.id) - .await - .unwrap(); + db.create_scope( + &user.id, + false, + &scope_name, + user.id, + &ScopeDescription::default(), + ) + .await + .unwrap(); let CreatePackageResult::Ok(package) = db.create_package(&scope_name, &package_name).await.unwrap() diff --git a/api/src/ids.rs b/api/src/ids.rs index 4dc0a91a..2d43aca2 100644 --- a/api/src/ids.rs +++ b/api/src/ids.rs @@ -144,6 +144,126 @@ pub enum ScopeNameValidateError { DoubleHyphens, } +/// A scope description, like 'This is a user scope' or 'Admin scope'. +/// The description must be at least 5 characters long, and at most 200 characters and can be empty. +/// The description can contain utf-8 characters, including emoji. +#[derive(Clone, PartialEq, Eq, Hash)] +pub struct ScopeDescription(String); + +impl ScopeDescription { + pub fn new( + description: String, + ) -> Result { + if description.len() > 200 { + return Err(ScopeDescriptionValidateError::TooLong); + } + + if description.len() < 5 && !description.is_empty() { + return Err(ScopeDescriptionValidateError::TooShort); + } + + Ok(ScopeDescription(description)) + } + + pub fn default() -> Self { + ScopeDescription("".to_owned()) + } +} + +impl TryFrom<&str> for ScopeDescription { + type Error = ScopeDescriptionValidateError; + fn try_from(value: &str) -> Result { + Self::new(value.to_owned()) + } +} + +impl TryFrom for ScopeDescription { + type Error = ScopeDescriptionValidateError; + fn try_from(value: String) -> Result { + Self::new(value) + } +} + +impl std::fmt::Display for ScopeDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl std::fmt::Debug for ScopeDescription { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl<'a> serde::Deserialize<'a> for ScopeDescription { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'a>, + { + let s: String = String::deserialize(deserializer)?; + Self::new(s).map_err(serde::de::Error::custom) + } +} + +impl serde::Serialize for ScopeDescription { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + +impl sqlx::Decode<'_, Postgres> for ScopeDescription { + fn decode( + value: PgValueRef<'_>, + ) -> Result> { + let s: String = sqlx::Decode::<'_, Postgres>::decode(value)?; + Self::new(s).map_err(|e| Box::new(e) as _) + } +} + +impl<'q> sqlx::Encode<'q, Postgres> for ScopeDescription { + fn encode_by_ref( + &self, + buf: &mut >::ArgumentBuffer, + ) -> sqlx::encode::IsNull { + >::encode_by_ref( + &self.0, buf, + ) + } +} + +impl sqlx::Type for ScopeDescription { + fn type_info() -> ::TypeInfo { + >::type_info() + } +} + +impl sqlx::postgres::PgHasArrayType for ScopeDescription { + fn array_type_info() -> sqlx::postgres::PgTypeInfo { + ::array_type_info() + } +} + +impl std::ops::Deref for ScopeDescription { + type Target = String; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Debug, Clone, Error)] +pub enum ScopeDescriptionValidateError { + #[error("scope description must be at most 200 characters long")] + TooLong, + + #[error("scope description must be at least 5 character long")] + TooShort, +} + /// A package name, like 'foo' or 'bar'. The name is not prefixed with an @. /// The name must be at least 2 character long, and at most 58 characters long. /// The name must only contain alphanumeric characters and hyphens. diff --git a/api/src/tasks.rs b/api/src/tasks.rs index 0da287dc..f8788650 100644 --- a/api/src/tasks.rs +++ b/api/src/tasks.rs @@ -466,9 +466,7 @@ mod tests { use crate::db::NewPackageVersion; use crate::db::PackageVersionMeta; use crate::gcp::BigQueryQueryResult; - use crate::ids::PackageName; - use crate::ids::ScopeName; - use crate::ids::Version; + use crate::ids::{PackageName, ScopeDescription, ScopeName, Version}; use super::deserialize_version_download_count_from_bigquery; @@ -551,12 +549,24 @@ mod tests { let v0_219_3 = Version::new("0.219.3").unwrap(); let v1_0_0 = Version::new("1.0.0").unwrap(); - db.create_scope(&Uuid::nil(), false, &std, Uuid::nil()) - .await - .unwrap(); - db.create_scope(&Uuid::nil(), false, &luca, Uuid::nil()) - .await - .unwrap(); + db.create_scope( + &Uuid::nil(), + false, + &std, + Uuid::nil(), + &ScopeDescription::default(), + ) + .await + .unwrap(); + db.create_scope( + &Uuid::nil(), + false, + &luca, + Uuid::nil(), + &ScopeDescription::default(), + ) + .await + .unwrap(); db.create_package(&std, &fs).await.unwrap(); db.create_package(&luca, &flag).await.unwrap(); db.create_package_version_for_test(NewPackageVersion { diff --git a/api/src/util.rs b/api/src/util.rs index 77ce011c..94f696ae 100644 --- a/api/src/util.rs +++ b/api/src/util.rs @@ -426,6 +426,7 @@ pub mod test { use crate::errors_internal::ApiErrorStruct; use crate::gcp::FakeGcsTester; use crate::util::sanitize_redirect_url; + use crate::ids::ScopeDescription; use crate::ApiError; use crate::MainRouterOptions; use hyper::http::HeaderName; @@ -552,9 +553,15 @@ pub mod test { let scope_name = "scope".try_into().unwrap(); - db.create_scope(&user1.user.id, false, &scope_name, user1.user.id) - .await - .unwrap(); + db.create_scope( + &user1.user.id, + false, + &scope_name, + user1.user.id, + &ScopeDescription::default(), + ) + .await + .unwrap(); let (scope, _, _) = db .update_scope_limits( &staff_user.user.id, diff --git a/frontend/islands/new.tsx b/frontend/islands/new.tsx index 1f3cacbb..ecd8f3d2 100644 --- a/frontend/islands/new.tsx +++ b/frontend/islands/new.tsx @@ -5,12 +5,16 @@ import { useSignal, useSignalEffect, } from "@preact/signals"; -import { Package, Scope, User } from "../utils/api_types.ts"; -import { api, path } from "../utils/api.ts"; import { ComponentChildren } from "preact"; import twas from "twas"; +import { api, path } from "../utils/api.ts"; +import { + validatePackageName, + validateScopeDescription, + validateScopeName, +} from "../utils/ids.ts"; import { TicketModal } from "./TicketModal.tsx"; - +import type { Package, Scope, User } from "../utils/api_types.ts"; interface IconColorProps { done: Signal; children: ComponentChildren; @@ -158,27 +162,24 @@ export function ScopeSelect( function CreateScope( props: { initialValue: string | undefined; - onCreate: (scope: string) => void; + onCreate: (scope: string, description: string) => void; locked: boolean; user: User; }, ) { const newScope = useSignal(props.initialValue ?? ""); + const description = useSignal(""); const errorCode = useSignal(""); const error = useSignal(""); const message = useComputed(() => { if (error.value) return error.value; - if (newScope.value.length === 0) { - return ""; - } - if (newScope.value.length > 20) { - return "Scope name cannot be longer than 20 characters."; - } - if (!/^[a-z0-9\-]+$/.test(newScope.value)) { - return "Scope name can only contain lowercase letters, numbers, and hyphens."; + const validationError = validateScopeName(newScope.value); + if (validationError) { + return validationError; } - if (/^-/.test(newScope.value)) { - return "Scope name must start with a letter or number."; + const descriptionError = validateScopeDescription(description.value); + if (descriptionError) { + return descriptionError; } return ""; }); @@ -188,9 +189,10 @@ function CreateScope( const resp = await api.post(path`/scopes`, { scope: newScope.value, + description: description.value, }); if (resp.ok) { - props.onCreate(newScope.value); + props.onCreate(newScope.value, description.value); } else { console.error(resp); errorCode.value = resp.code; @@ -224,6 +226,28 @@ function CreateScope( }} /> +