diff --git a/crates/handlers/src/admin/params.rs b/crates/handlers/src/admin/params.rs index 633917d9a..67ec3316f 100644 --- a/crates/handlers/src/admin/params.rs +++ b/crates/handlers/src/admin/params.rs @@ -7,7 +7,7 @@ // Generated code from schemars violates this rule #![allow(clippy::str_to_string)] -use std::num::NonZeroUsize; +use std::{borrow::Cow, num::NonZeroUsize}; use aide::OperationIo; use axum::{ @@ -64,6 +64,34 @@ impl std::ops::Deref for UlidPathParam { /// The default page size if not specified const DEFAULT_PAGE_SIZE: usize = 10; +#[derive(Deserialize, JsonSchema, Clone, Copy, Default, Debug)] +pub enum IncludeCount { + /// Include the total number of items (default) + #[default] + #[serde(rename = "true")] + True, + + /// Do not include the total number of items + #[serde(rename = "false")] + False, + + /// Only include the total number of items, skip the items themselves + #[serde(rename = "only")] + Only, +} + +impl IncludeCount { + pub(crate) fn add_to_base(self, base: &str) -> Cow<'_, str> { + let separator = if base.contains('?') { '&' } else { '?' }; + match self { + // This is the default, don't add anything + Self::True => Cow::Borrowed(base), + Self::False => format!("{base}{separator}count=false").into(), + Self::Only => format!("{base}{separator}count=only").into(), + } + } +} + #[derive(Deserialize, JsonSchema, Clone, Copy)] struct PaginationParams { /// Retrieve the items before the given ID @@ -83,6 +111,10 @@ struct PaginationParams { /// Retrieve the last N items #[serde(rename = "page[last]")] last: Option, + + /// Include the total number of items. Defaults to `true`. + #[serde(rename = "count")] + include_count: Option, } #[derive(Debug, thiserror::Error)] @@ -107,7 +139,7 @@ impl IntoResponse for PaginationRejection { /// An extractor for pagination parameters in the query string #[derive(OperationIo, Debug, Clone, Copy)] #[aide(input_with = "Query")] -pub struct Pagination(pub mas_storage::Pagination); +pub struct Pagination(pub mas_storage::Pagination, pub IncludeCount); impl FromRequestParts for Pagination { type Rejection = PaginationRejection; @@ -130,11 +162,14 @@ impl FromRequestParts for Pagination { (None, Some(last)) => (PaginationDirection::Backward, last.into()), }; - Ok(Self(mas_storage::Pagination { - before: params.before, - after: params.after, - direction, - count, - })) + Ok(Self( + mas_storage::Pagination { + before: params.before, + after: params.after, + direction, + count, + }, + params.include_count.unwrap_or_default(), + )) } } diff --git a/crates/handlers/src/admin/response.rs b/crates/handlers/src/admin/response.rs index 19f0e8040..c5065c7da 100644 --- a/crates/handlers/src/admin/response.rs +++ b/crates/handlers/src/admin/response.rs @@ -21,10 +21,12 @@ struct PaginationLinks { self_: String, /// The link to the first page of results - first: String, + #[serde(skip_serializing_if = "Option::is_none")] + first: Option, /// The link to the last page of results - last: String, + #[serde(skip_serializing_if = "Option::is_none")] + last: Option, /// The link to the next page of results /// @@ -42,17 +44,26 @@ struct PaginationLinks { #[derive(Serialize, JsonSchema)] struct PaginationMeta { /// The total number of results - count: usize, + #[serde(skip_serializing_if = "Option::is_none")] + count: Option, +} + +impl PaginationMeta { + fn is_empty(&self) -> bool { + self.count.is_none() + } } /// A top-level response with a page of resources #[derive(Serialize, JsonSchema)] pub struct PaginatedResponse { /// Response metadata + #[serde(skip_serializing_if = "PaginationMeta::is_empty")] meta: PaginationMeta, /// The list of resources - data: Vec>, + #[serde(skip_serializing_if = "Option::is_none")] + data: Option>>, /// Related links links: PaginationLinks, @@ -87,16 +98,22 @@ fn url_with_pagination(base: &str, pagination: Pagination) -> String { } impl PaginatedResponse { - pub fn new( + pub fn for_page( page: mas_storage::Page, current_pagination: Pagination, - count: usize, + count: Option, base: &str, ) -> Self { let links = PaginationLinks { self_: url_with_pagination(base, current_pagination), - first: url_with_pagination(base, Pagination::first(current_pagination.count)), - last: url_with_pagination(base, Pagination::last(current_pagination.count)), + first: Some(url_with_pagination( + base, + Pagination::first(current_pagination.count), + )), + last: Some(url_with_pagination( + base, + Pagination::last(current_pagination.count), + )), next: page.has_next_page.then(|| { url_with_pagination( base, @@ -121,7 +138,23 @@ impl PaginatedResponse { Self { meta: PaginationMeta { count }, - data, + data: Some(data), + links, + } + } + + pub fn for_count_only(count: usize, base: &str) -> Self { + let links = PaginationLinks { + self_: base.to_owned(), + first: None, + last: None, + next: None, + prev: None, + }; + + Self { + meta: PaginationMeta { count: Some(count) }, + data: None, links, } } diff --git a/crates/handlers/src/admin/v1/compat_sessions/list.rs b/crates/handlers/src/admin/v1/compat_sessions/list.rs index debb2a304..96a2708af 100644 --- a/crates/handlers/src/admin/v1/compat_sessions/list.rs +++ b/crates/handlers/src/admin/v1/compat_sessions/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{CompatSession, Resource}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -143,10 +143,10 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }; t.description("Paginated response of compatibility sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), CompatSession::PATH, )) }) @@ -159,10 +159,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.compat_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = CompatSession::PATH); + let base = include_count.add_to_base(&base); let filter = CompatSessionFilter::default(); // Load the user from the filter @@ -206,15 +207,31 @@ pub async fn handler( None => filter, }; - let page = repo.compat_session().list(filter, pagination).await?; - let count = repo.compat_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .compat_session() + .list(filter, pagination) + .await? + .map(CompatSession::from); + let count = repo.compat_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .compat_session() + .list(filter, pagination) + .await? + .map(CompatSession::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.compat_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(CompatSession::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs index 52b597edc..8b6ff6b2b 100644 --- a/crates/handlers/src/admin/v1/oauth2_sessions/list.rs +++ b/crates/handlers/src/admin/v1/oauth2_sessions/list.rs @@ -25,7 +25,7 @@ use crate::{ admin::{ call_context::CallContext, model::{OAuth2Session, Resource}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -198,10 +198,10 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }; t.description("Paginated response of OAuth 2.0 sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), OAuth2Session::PATH, )) }) @@ -218,10 +218,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.oauth2_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = OAuth2Session::PATH); + let base = include_count.add_to_base(&base); let filter = OAuth2SessionFilter::default(); // Load the user from the filter @@ -300,15 +301,31 @@ pub async fn handler( None => filter, }; - let page = repo.oauth2_session().list(filter, pagination).await?; - let count = repo.oauth2_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .oauth2_session() + .list(filter, pagination) + .await? + .map(OAuth2Session::from); + let count = repo.oauth2_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .oauth2_session() + .list(filter, pagination) + .await? + .map(OAuth2Session::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.oauth2_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(OAuth2Session::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs index 59efe6541..068011ab7 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_links/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UpstreamOAuthLink}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -118,10 +118,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }; t.description("Paginated response of upstream OAuth 2.0 links") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UpstreamOAuthLink::PATH, )) }) @@ -135,10 +135,11 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.upstream_oauth_links.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UpstreamOAuthLink::PATH); + let base = include_count.add_to_base(&base); let filter = UpstreamOAuthLinkFilter::default(); // Load the user from the filter @@ -183,15 +184,31 @@ pub async fn handler( filter }; - let page = repo.upstream_oauth_link().list(filter, pagination).await?; - let count = repo.upstream_oauth_link().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .upstream_oauth_link() + .list(filter, pagination) + .await? + .map(UpstreamOAuthLink::from); + let count = repo.upstream_oauth_link().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .upstream_oauth_link() + .list(filter, pagination) + .await? + .map(UpstreamOAuthLink::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.upstream_oauth_link().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UpstreamOAuthLink::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs index dc5f2cc9c..419c3552f 100644 --- a/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs +++ b/crates/handlers/src/admin/v1/upstream_oauth_providers/list.rs @@ -20,7 +20,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UpstreamOAuthProvider}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -90,10 +90,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }; t.description("Paginated response of upstream OAuth 2.0 providers") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UpstreamOAuthProvider::PATH, )) }) @@ -102,10 +102,11 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.upstream_oauth_providers.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UpstreamOAuthProvider::PATH); + let base = include_count.add_to_base(&base); let filter = UpstreamOAuthProviderFilter::new(); let filter = match params.enabled { @@ -114,18 +115,31 @@ pub async fn handler( None => filter, }; - let page = repo - .upstream_oauth_provider() - .list(filter, pagination) - .await?; - let count = repo.upstream_oauth_provider().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .upstream_oauth_provider() + .list(filter, pagination) + .await? + .map(UpstreamOAuthProvider::from); + let count = repo.upstream_oauth_provider().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .upstream_oauth_provider() + .list(filter, pagination) + .await? + .map(UpstreamOAuthProvider::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.upstream_oauth_provider().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UpstreamOAuthProvider::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/user_emails/list.rs b/crates/handlers/src/admin/v1/user_emails/list.rs index 92dfe12c2..2363aba28 100644 --- a/crates/handlers/src/admin/v1/user_emails/list.rs +++ b/crates/handlers/src/admin/v1/user_emails/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserEmail}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -105,10 +105,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }; t.description("Paginated response of user emails") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserEmail::PATH, )) }) @@ -121,10 +121,11 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { #[tracing::instrument(name = "handler.admin.v1.user_emails.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserEmail::PATH); + let base = include_count.add_to_base(&base); let filter = UserEmailFilter::default(); // Load the user from the filter @@ -150,15 +151,31 @@ pub async fn handler( None => filter, }; - let page = repo.user_email().list(filter, pagination).await?; - let count = repo.user_email().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .user_email() + .list(filter, pagination) + .await? + .map(UserEmail::from); + let count = repo.user_email().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .user_email() + .list(filter, pagination) + .await? + .map(UserEmail::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user_email().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UserEmail::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs index 546491536..f1700aaa7 100644 --- a/crates/handlers/src/admin/v1/user_registration_tokens/list.rs +++ b/crates/handlers/src/admin/v1/user_registration_tokens/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserRegistrationToken}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -118,10 +118,10 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }; t.description("Paginated response of registration tokens") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserRegistrationToken::PATH, )) }) @@ -132,10 +132,11 @@ pub async fn handler( CallContext { mut repo, clock, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserRegistrationToken::PATH); + let base = include_count.add_to_base(&base); let now = clock.now(); let mut filter = UserRegistrationTokenFilter::new(now); @@ -155,18 +156,31 @@ pub async fn handler( filter = filter.with_valid(valid); } - let page = repo - .user_registration_token() - .list(filter, pagination) - .await?; - let count = repo.user_registration_token().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .user_registration_token() + .list(filter, pagination) + .await? + .map(|token| UserRegistrationToken::new(token, now)); + let count = repo.user_registration_token().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .user_registration_token() + .list(filter, pagination) + .await? + .map(|token| UserRegistrationToken::new(token, now)); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user_registration_token().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(|token| UserRegistrationToken::new(token, now)), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/user_sessions/list.rs b/crates/handlers/src/admin/v1/user_sessions/list.rs index 28a52edf2..6ba1dc5f0 100644 --- a/crates/handlers/src/admin/v1/user_sessions/list.rs +++ b/crates/handlers/src/admin/v1/user_sessions/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, UserSession}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -129,10 +129,10 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p }; t.description("Paginated response of user sessions") - .example(PaginatedResponse::new( + .example(PaginatedResponse::for_page( page, pagination, - 42, + Some(42), UserSession::PATH, )) }) @@ -145,10 +145,11 @@ Use the `filter[status]` parameter to filter the sessions by their status and `p #[tracing::instrument(name = "handler.admin.v1.user_sessions.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = UserSession::PATH); + let base = include_count.add_to_base(&base); let filter = BrowserSessionFilter::default(); // Load the user from the filter @@ -175,15 +176,31 @@ pub async fn handler( None => filter, }; - let page = repo.browser_session().list(filter, pagination).await?; - let count = repo.browser_session().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo + .browser_session() + .list(filter, pagination) + .await? + .map(UserSession::from); + let count = repo.browser_session().count(filter).await?; + PaginatedResponse::for_page(page, pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo + .browser_session() + .list(filter, pagination) + .await? + .map(UserSession::from); + PaginatedResponse::for_page(page, pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.browser_session().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(UserSession::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } #[cfg(test)] diff --git a/crates/handlers/src/admin/v1/users/list.rs b/crates/handlers/src/admin/v1/users/list.rs index da70e5807..ddafa9984 100644 --- a/crates/handlers/src/admin/v1/users/list.rs +++ b/crates/handlers/src/admin/v1/users/list.rs @@ -21,7 +21,7 @@ use crate::{ admin::{ call_context::CallContext, model::{Resource, User}, - params::Pagination, + params::{IncludeCount, Pagination}, response::{ErrorResponse, PaginatedResponse}, }, impl_from_error_for_route, @@ -143,17 +143,23 @@ pub fn doc(operation: TransformOperation) -> TransformOperation { }; t.description("Paginated response of users") - .example(PaginatedResponse::new(page, pagination, 42, User::PATH)) + .example(PaginatedResponse::for_page( + page, + pagination, + Some(42), + User::PATH, + )) }) } #[tracing::instrument(name = "handler.admin.v1.users.list", skip_all)] pub async fn handler( CallContext { mut repo, .. }: CallContext, - Pagination(pagination): Pagination, + Pagination(pagination, include_count): Pagination, params: FilterParams, ) -> Result>, RouteError> { let base = format!("{path}{params}", path = User::PATH); + let base = include_count.add_to_base(&base); let filter = UserFilter::default(); let filter = match params.admin { @@ -180,13 +186,21 @@ pub async fn handler( None => filter, }; - let page = repo.user().list(filter, pagination).await?; - let count = repo.user().count(filter).await?; + let response = match include_count { + IncludeCount::True => { + let page = repo.user().list(filter, pagination).await?; + let count = repo.user().count(filter).await?; + PaginatedResponse::for_page(page.map(User::from), pagination, Some(count), &base) + } + IncludeCount::False => { + let page = repo.user().list(filter, pagination).await?; + PaginatedResponse::for_page(page.map(User::from), pagination, None, &base) + } + IncludeCount::Only => { + let count = repo.user().count(filter).await?; + PaginatedResponse::for_count_only(count, &base) + } + }; - Ok(Json(PaginatedResponse::new( - page.map(User::from), - pagination, - count, - &base, - ))) + Ok(Json(response)) } diff --git a/docs/api/spec.json b/docs/api/spec.json index 166436454..d874f7700 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -107,6 +107,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[user]", @@ -373,6 +384,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[user]", @@ -893,6 +915,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[admin]", @@ -1752,6 +1785,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[user]", @@ -2097,6 +2141,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[user]", @@ -2335,6 +2390,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[used]", @@ -2882,6 +2948,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[user]", @@ -3279,6 +3356,17 @@ }, "style": "form" }, + { + "in": "query", + "name": "count", + "description": "Include the total number of items. Defaults to `true`.", + "schema": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true + }, + "style": "form" + }, { "in": "query", "name": "filter[enabled]", @@ -3481,6 +3569,11 @@ "format": "uint", "minimum": 1.0, "nullable": true + }, + "count": { + "description": "Include the total number of items. Defaults to `true`.", + "$ref": "#/components/schemas/IncludeCount", + "nullable": true } } }, @@ -3494,6 +3587,31 @@ "type": "string", "pattern": "^[0123456789ABCDEFGHJKMNPQRSTVWXYZ]{26}$" }, + "IncludeCount": { + "oneOf": [ + { + "description": "Include the total number of items (default)", + "type": "string", + "enum": [ + "true" + ] + }, + { + "description": "Do not include the total number of items", + "type": "string", + "enum": [ + "false" + ] + }, + { + "description": "Only include the total number of items, skip the items themselves", + "type": "string", + "enum": [ + "only" + ] + } + ] + }, "CompatSessionFilter": { "type": "object", "properties": { @@ -3525,7 +3643,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -3539,7 +3656,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_CompatSession" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -3549,15 +3667,13 @@ }, "PaginationMeta": { "type": "object", - "required": [ - "count" - ], "properties": { "count": { "description": "The total number of results", "type": "integer", "format": "uint", - "minimum": 0.0 + "minimum": 0.0, + "nullable": true } } }, @@ -3678,8 +3794,6 @@ "description": "Related links", "type": "object", "required": [ - "first", - "last", "self" ], "properties": { @@ -3689,11 +3803,13 @@ }, "first": { "description": "The link to the first page of results", - "type": "string" + "type": "string", + "nullable": true }, "last": { "description": "The link to the last page of results", - "type": "string" + "type": "string", + "nullable": true }, "next": { "description": "The link to the next page of results\n\nOnly present if there is a next page", @@ -3820,7 +3936,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -3834,7 +3949,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_OAuth2Session" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4065,7 +4181,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4079,7 +4194,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_User" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4266,7 +4382,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4280,7 +4395,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_UserEmail" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4401,7 +4517,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4415,7 +4530,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_UserSession" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4538,7 +4654,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4552,7 +4667,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_UserRegistrationToken" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4727,7 +4843,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4741,7 +4856,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthLink" - } + }, + "nullable": true }, "links": { "description": "Related links", @@ -4869,7 +4985,6 @@ "description": "A top-level response with a page of resources", "type": "object", "required": [ - "data", "links", "meta" ], @@ -4883,7 +4998,8 @@ "type": "array", "items": { "$ref": "#/components/schemas/SingleResource_for_UpstreamOAuthProvider" - } + }, + "nullable": true }, "links": { "description": "Related links",