From 698ab7709daef3e1fb5962db9c0ebb7d6e978df0 Mon Sep 17 00:00:00 2001 From: Eugie Limpin Date: Fri, 12 Dec 2025 15:27:49 +0800 Subject: [PATCH 1/2] Add expires_before arg and count support to user PAT GraphQL endpoint Add expires_before argument to PersonalAccessTokensResolver to enable filtering tokens that expire before a specified date. Also add countable connection support to provide total count functionality. --- .../users/personal_access_tokens_resolver.rb | 7 +++- .../personal_access_token_type.rb | 2 + doc/api/graphql/reference/_index.md | 41 +++++++++++++++---- .../personal_access_tokens_query_spec.rb | 24 +++++++++++ 4 files changed, 64 insertions(+), 10 deletions(-) diff --git a/app/graphql/resolvers/users/personal_access_tokens_resolver.rb b/app/graphql/resolvers/users/personal_access_tokens_resolver.rb index 566a99357f2b0d..4e16d510e9fabc 100644 --- a/app/graphql/resolvers/users/personal_access_tokens_resolver.rb +++ b/app/graphql/resolvers/users/personal_access_tokens_resolver.rb @@ -33,9 +33,13 @@ class PersonalAccessTokensResolver < BaseResolver required: false, description: 'Filter personal access tokens by their revoked status.' + argument :expires_before, Types::DateType, + required: false, + description: 'Filter personal access tokens that expire before the specified date.' + argument :expires_after, Types::DateType, required: false, - description: 'Filter personal access tokens that expire after the timestamp.' + description: 'Filter personal access tokens that expire after the specified date.' argument :created_after, Types::TimeType, required: false, @@ -71,6 +75,7 @@ def filter_params(args) state: args[:state], sort: args[:sort], expires_after: args[:expires_after], + expires_before: args[:expires_before], created_after: args[:created_after], last_used_after: args[:last_used_after] }.tap do |params| diff --git a/app/graphql/types/authz/personal_access_tokens/personal_access_token_type.rb b/app/graphql/types/authz/personal_access_tokens/personal_access_token_type.rb index 3f885227437d6f..673b177406feff 100644 --- a/app/graphql/types/authz/personal_access_tokens/personal_access_token_type.rb +++ b/app/graphql/types/authz/personal_access_tokens/personal_access_token_type.rb @@ -8,6 +8,8 @@ class PersonalAccessTokenType < BaseObject graphql_name 'PersonalAccessToken' description 'Personal access token.' + connection_type_class Types::CountableConnectionType + field :id, GraphQL::Types::ID, null: false, diff --git a/doc/api/graphql/reference/_index.md b/doc/api/graphql/reference/_index.md index 98899c853b8b17..49e90b0234d565 100644 --- a/doc/api/graphql/reference/_index.md +++ b/doc/api/graphql/reference/_index.md @@ -21910,6 +21910,20 @@ The connection type for [`PersonalAccessToken`](#personalaccesstoken). | `nodes` | [`[PersonalAccessToken]`](#personalaccesstoken) | A list of nodes. | | `pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | +##### Fields with arguments + +###### `PersonalAccessTokenConnection.count` + +Total count of collection. Returns limit + 1 for counts greater than the limit. + +Returns [`Int!`](#int). + +####### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| `limit` | [`Int`](#int) | Limit applied to the count query, returns limit + 1. When not provided, returns the exact count. | + #### `PersonalAccessTokenEdge` The edge type for [`PersonalAccessToken`](#personalaccesstoken). @@ -25089,7 +25103,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -26781,7 +26796,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -30152,7 +30168,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -37714,7 +37731,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -38171,7 +38189,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -38679,7 +38698,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -39155,7 +39175,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -47838,7 +47859,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | @@ -58184,7 +58206,8 @@ four standard [pagination arguments](#pagination-arguments): | Name | Type | Description | | ---- | ---- | ----------- | | `createdAfter` | [`Time`](#time) | Filter personal access tokens created after the timestamp. | -| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the timestamp. | +| `expiresAfter` | [`Date`](#date) | Filter personal access tokens that expire after the specified date. | +| `expiresBefore` | [`Date`](#date) | Filter personal access tokens that expire before the specified date. | | `lastUsedAfter` | [`Time`](#time) | Filter personal access tokens last used after the timestamp. | | `revoked` | [`Boolean`](#boolean) | Filter personal access tokens by their revoked status. | | `search` | [`String`](#string) | Query to search personal access tokens by name. | diff --git a/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb b/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb index 4b014edd18696b..3c17c0131d7c78 100644 --- a/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb +++ b/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb @@ -15,6 +15,7 @@ let_it_be(:legacy_token_revoked) { create(:personal_access_token, :revoked, user: user, name: 'Revoked token') } let_it_be(:legacy_token_expired) { create(:personal_access_token, :expired, :with_last_used_ips, user:) } + let_it_be(:legacy_token_expiring_soon) { create(:personal_access_token, user: user, expires_at: 1.week.from_now) } let_it_be(:granular_token) do create(:granular_pat, name: 'Special token', last_used_at: 1.day.ago, permissions: ['read_member_role'], user: user, namespace: group) @@ -109,6 +110,10 @@ 'revoked' => true, 'active' => false }), + a_hash_including({ + 'name' => legacy_token_expiring_soon.name, + 'active' => true + }), a_hash_including({ 'name' => legacy_token_expired.name, 'active' => false, @@ -117,6 +122,16 @@ ) end + describe 'count' do + let(:fields) { 'count' } + + it 'is available' do + send_query + + expect(graphql_data_at(*%i[user personalAccessTokens count])).to eq 5 + end + end + it 'avoids N+1 queries' do control = ActiveRecord::QueryRecorder.new(skip_cached: false) do post_graphql(query, current_user: current_user) @@ -195,6 +210,15 @@ end end + context 'with { expires_before: }' do + let(:args) { { expires_before: 2.weeks.from_now.to_date } } + + it 'returns only personal access tokens that expire before the given date' do + expires_at_dates = personal_access_tokens_data.pluck('expiresAt').map(&:to_date) + expect(expires_at_dates).to all(be <= args[:expires_before]) + end + end + context 'with { expires_after: }' do let(:args) { { expires_after: 50.days.from_now.to_date } } -- GitLab From 40c96aa9673d28fab570bf0c595d4b1232027c47 Mon Sep 17 00:00:00 2001 From: Eugie Limpin Date: Mon, 15 Dec 2025 09:16:42 +0800 Subject: [PATCH 2/2] Improve spec description --- .../personal_access_tokens_query_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb b/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb index 3c17c0131d7c78..087bccca933771 100644 --- a/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb +++ b/spec/requests/api/graphql/authz/personal_access_tokens/personal_access_tokens_query_spec.rb @@ -122,10 +122,10 @@ ) end - describe 'count' do + describe 'count field' do let(:fields) { 'count' } - it 'is available' do + it 'returns the count of PersonalAccessTokens' do send_query expect(graphql_data_at(*%i[user personalAccessTokens count])).to eq 5 -- GitLab