Admin API: parameter to include total number of items

This allows removing the count calculation when not needed, or to skip
the list of items entirely.
This commit is contained in:
Quentin Gliech
2025-09-29 18:27:22 +02:00
parent 007136c990
commit 39fc59deb2
11 changed files with 453 additions and 142 deletions

View File

@@ -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<NonZeroUsize>,
/// Include the total number of items. Defaults to `true`.
#[serde(rename = "count")]
include_count: Option<IncludeCount>,
}
#[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<PaginationParams>")]
pub struct Pagination(pub mas_storage::Pagination);
pub struct Pagination(pub mas_storage::Pagination, pub IncludeCount);
impl<S: Send + Sync> FromRequestParts<S> for Pagination {
type Rejection = PaginationRejection;
@@ -130,11 +162,14 @@ impl<S: Send + Sync> FromRequestParts<S> 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(),
))
}
}

View File

@@ -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<String>,
/// The link to the last page of results
last: String,
#[serde(skip_serializing_if = "Option::is_none")]
last: Option<String>,
/// 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<usize>,
}
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<T> {
/// Response metadata
#[serde(skip_serializing_if = "PaginationMeta::is_empty")]
meta: PaginationMeta,
/// The list of resources
data: Vec<SingleResource<T>>,
#[serde(skip_serializing_if = "Option::is_none")]
data: Option<Vec<SingleResource<T>>>,
/// Related links
links: PaginationLinks,
@@ -87,16 +98,22 @@ fn url_with_pagination(base: &str, pagination: Pagination) -> String {
}
impl<T: Resource> PaginatedResponse<T> {
pub fn new(
pub fn for_page(
page: mas_storage::Page<T>,
current_pagination: Pagination,
count: usize,
count: Option<usize>,
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<T: Resource> PaginatedResponse<T> {
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,
}
}

View File

@@ -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<Json<PaginatedResponse<CompatSession>>, 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)]

View File

@@ -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<Json<PaginatedResponse<OAuth2Session>>, 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)]

View File

@@ -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<Json<PaginatedResponse<UpstreamOAuthLink>>, 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)]

View File

@@ -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<Json<PaginatedResponse<UpstreamOAuthProvider>>, 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)]

View File

@@ -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<Json<PaginatedResponse<UserEmail>>, 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)]

View File

@@ -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<Json<PaginatedResponse<UserRegistrationToken>>, 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)]

View File

@@ -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<Json<PaginatedResponse<UserSession>>, 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)]

View File

@@ -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<Json<PaginatedResponse<User>>, 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))
}

View File

@@ -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",