From 5dddb9d561add4dbf3956afc7d33245888e01b1b Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 Jun 2026 14:43:37 +0200 Subject: [PATCH 1/2] Document publicationFilter for REST, GraphQL, and Document Service API. Adds dedicated reference pages for derived Draft & Publish cohorts, cross-links from status/parameters/draft-and-publish, and updates outdated count() guidance. --- docusaurus/docs/cms/api/document-service.md | 6 +- .../document-service/publication-filter.md | 245 +++++++++++++ .../docs/cms/api/document-service/status.md | 4 +- docusaurus/docs/cms/api/graphql.md | 56 +++ docusaurus/docs/cms/api/rest/parameters.md | 3 +- .../docs/cms/api/rest/publication-filter.md | 205 +++++++++++ docusaurus/docs/cms/api/rest/status.md | 2 + .../docs/cms/features/draft-and-publish.md | 5 +- docusaurus/sidebars.js | 2 + docusaurus/static/llms-code.txt | 295 ++++++++++++++++ docusaurus/static/llms-full.txt | 328 +++++++++++++++++- docusaurus/static/llms.txt | 2 + 12 files changed, 1147 insertions(+), 6 deletions(-) create mode 100644 docusaurus/docs/cms/api/document-service/publication-filter.md create mode 100644 docusaurus/docs/cms/api/rest/publication-filter.md diff --git a/docusaurus/docs/cms/api/document-service.md b/docusaurus/docs/cms/api/document-service.md index 75caa83314..cb0da597d3 100644 --- a/docusaurus/docs/cms/api/document-service.md +++ b/docusaurus/docs/cms/api/document-service.md @@ -101,6 +101,7 @@ Syntax: `findOne(parameters: Params) => Document` | `documentId` | Document id | | `ID` | | [`locale`](/cms/api/document-service/locale#find-one)| Locale of the document to find. | Default locale | String or `undefined` | | [`status`](/cms/api/document-service/status#find-one) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`fields`](/cms/api/document-service/fields#findone) | [Select fields](/cms/api/document-service/fields#findone) to return | All fields
(except those not populated by default) | Object | | [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | @@ -150,6 +151,7 @@ Syntax: `findFirst(parameters: Params) => Document` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#find-first) | Locale of the documents to find. | Default locale | String or `undefined` | | [`status`](/cms/api/document-service/status#find-first) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | | [`fields`](/cms/api/document-service/fields#findfirst) | [Select fields](/cms/api/document-service/fields#findfirst) to return | All fields
(except those not populate by default) | Object | | [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | @@ -240,6 +242,7 @@ Syntax: `findMany(parameters: Params) => Document[]` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#find-many) | Locale of the documents to find. | Default locale | String or `undefined` | | [`status`](/cms/api/document-service/status#find-many) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | | [`fields`](/cms/api/document-service/fields#findmany) | [Select fields](/cms/api/document-service/fields#findmany) to return | All fields
(except those not populate by default) | Object | | [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | @@ -793,12 +796,13 @@ Syntax: `count(parameters: Params) => number` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#count) | Locale of the documents to count | Default locale | String or `null` | | [`status`](/cms/api/document-service/status#count) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. -This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. There currently is no way to prevent already published documents from being counted. +This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: #### Examples diff --git a/docusaurus/docs/cms/api/document-service/publication-filter.md b/docusaurus/docs/cms/api/document-service/publication-filter.md new file mode 100644 index 0000000000..ef11cb1ecd --- /dev/null +++ b/docusaurus/docs/cms/api/document-service/publication-filter.md @@ -0,0 +1,245 @@ +--- +title: Using publicationFilter with the Document Service API +description: Use the publicationFilter parameter with Strapi's Document Service API to query derived Draft & Publish cohorts such as never-published or modified documents. +displayed_sidebar: cmsSidebar +sidebar_label: Publication filter +tags: +- API +- Content API +- count() +- Document Service API +- Draft & Publish +- findMany() +- findFirst() +- findOne() +- publicationFilter +- status +--- + +# Document Service API: `publicationFilter` + +The [`status`](/cms/api/document-service/status) parameter selects which **row slice** to read for each document: `draft` rows have `publishedAt: null`, and `published` rows have a non-null `publishedAt`. + +The optional `publicationFilter` parameter selects a **derived publication cohort** first: a set of `(documentId, locale)` pairs (or `documentId` only when [Internationalization (i18n)](/cms/features/internationalization) is disabled) defined by how draft and published rows relate. Strapi then returns the row that matches both the cohort and the resolved `status`. + +:::prerequisites +The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. If Draft & Publish is disabled, `publicationFilter` has no effect. +::: + +`publicationFilter` is supported on `findOne()`, `findFirst()`, `findMany()`, and `count()`. It can be combined with [`filters`](/cms/api/document-service/filters), [`populate`](/cms/api/document-service/populate), and other query parameters. Invalid values raise a validation error. + +## Default `status` when `publicationFilter` is used {#default-status} + +`publicationFilter` is applied **after** `status` is resolved (explicitly or by default). Defaults differ by API surface: + +| API surface | Default `status` when omitted | +| ----------- | ----------------------------- | +| Document Service API (direct) | `'draft'` | +| [REST API](/cms/api/rest/publication-filter) | `'published'` | +| [GraphQL API](/cms/api/graphql#publication-filter) | `PUBLISHED` | + +Example with `publicationFilter: 'modified'` and no `status`: + +```js +// Document Service API → draft rows in the modified cohort +await strapi.documents('api::restaurant.restaurant').findMany({ + publicationFilter: 'modified', +}); + +// REST: GET /api/restaurants?publicationFilter=modified → published rows in the modified cohort +``` + +Pair-scoped modes such as `never-published` only include draft rows in the cohort. With REST or GraphQL defaults (`status=published`), those queries return an empty result set unless you pass `status=draft` / `status: DRAFT`. + +## Available values {#values} + +REST and the Document Service API use kebab-case strings. GraphQL exposes the same cohorts through the [`PublicationFilter` enum](/cms/api/graphql#publication-filter). + +| Value | Scope | Cohort definition (which `(documentId, locale)` pairs match) | +| ----- | ----- | -------------------------------------------------------------- | +| `never-published` | Pair | No row with non-null `publishedAt` exists for the same `(documentId, locale)` | +| `has-published-version` | Pair | **Both** a draft row and a published row exist for the same `(documentId, locale)` | +| `modified` | Pair | Both slices exist and `draft.updatedAt > published.updatedAt` | +| `unmodified` | Pair | Both slices exist and `draft.updatedAt <= published.updatedAt` | +| `never-published-document` | Document | No row with non-null `publishedAt` exists for the same `documentId` in **any** locale | +| `has-published-version-document` | Document | At least one published row exists for the same `documentId` in **any** locale | +| `published-without-draft` | Pair | A published row exists for the pair and **no** draft row exists for the same `(documentId, locale)` | +| `published-with-draft` | Pair | A published row exists for the pair and a draft row **also** exists for the same `(documentId, locale)` | + +For content-types without i18n, read `(documentId, locale)` as `documentId` only. + +### Semantics notes {#semantics} + +- **`has-published-version` excludes orphan published rows**: If only a published row exists for a pair (no draft sibling), that pair is **not** in the `has-published-version` cohort. Orphan published rows can appear under `published-without-draft` when querying with `status: 'published'`. +- **`modified` / `unmodified` require both slices**: Pairs with only a draft or only a published row are not included. +- **`modified` ∪ `unmodified` = `has-published-version`** (for the same `status`): The two modes partition pairs that have both slices. +- **Document-scoped modes**: Existence checks use `documentId` only. A document with draft EN + published NL qualifies for `has-published-version-document` even though EN is never published at the pair level. +- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They are degenerate (empty) with `status: 'draft'`. + +### Content Manager list filters {#content-manager} + +The Content Manager **Status** filter (`__status`) is translated server-side. Only the **Draft (never published)** option uses `publicationFilter`: + +| Content Manager filter | Document Service query equivalent | +| ---------------------- | --------------------------------- | +| Draft (never published) | `status: 'draft'`, `publicationFilter: 'never-published-document'` | +| Published (all) | `status: 'published'` (no `publicationFilter`) | +| Published (modified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter); similar intent to `status: 'published'` + `publicationFilter: 'modified'` but implemented separately in the Content Manager API | +| Published (unmodified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter) | + +The **Draft (never published)** filter is document-scoped (`never-published-document`), not pair-scoped `never-published`. + +## Combine `status` and `publicationFilter` {#status-combination} + +| `status` | `publicationFilter` | Rows returned | +| -------- | ------------------- | ------------- | +| `draft` | `never-published` | Draft rows for pairs never published in that locale | +| `published` | `never-published` | Empty (degenerate) | +| `draft` | `has-published-version` | Draft rows for pairs that also have a published version | +| `published` | `has-published-version` | Published rows for pairs that also have a draft version (excludes orphan published-only pairs) | +| `draft` | `modified` | Draft rows newer than their published peer | +| `published` | `modified` | Published rows whose draft peer is newer | +| `draft` | `unmodified` | Draft rows not newer than their published peer | +| `published` | `unmodified` | Published rows whose draft peer is not newer | +| `draft` | `never-published-document` | Draft rows whose document has no published row in any locale | +| `published` | `never-published-document` | Empty (degenerate) | +| `draft` | `has-published-version-document` | Draft rows whose document has at least one published row (any locale) | +| `published` | `has-published-version-document` | Published rows whose document has at least one draft row (any locale) | +| `published` | `published-without-draft` | Published rows with no draft sibling for the same pair | +| `draft` | `published-without-draft` | Empty (degenerate) | +| `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | +| `draft` | `published-with-draft` | Empty (degenerate) | + +Valid but empty combinations do not return validation errors. + +## Query never-published drafts {#never-published} + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published', +}); +``` + +Returns draft rows for `(documentId, locale)` pairs with no published version for that locale. + +## Query has-published-version drafts {#has-published-version} + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version', +}); +``` + +Returns draft rows where a published row also exists for the same `(documentId, locale)`. Does not return draft rows for pairs that only exist as orphan published rows. + +## Query modified or unmodified documents {#modified-unmodified} + +```js +// Draft side of modified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'modified', +}); + +// Published side of unmodified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'unmodified', +}); +``` + +Comparison uses `updatedAt` on the draft and published rows for the same pair. + +## Query document-scoped cohorts {#document-scoped} + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published-document', +}); +``` + +Returns draft rows for documents that have **never** been published in any locale. A multi-locale document with one published locale is excluded entirely, including its draft-only locales. + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version-document', +}); +``` + +Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped `has-published-version`). + +## Query published rows without or with a draft peer {#published-slice} + +```js +// Orphan published rows (published row, no draft sibling for the same pair) +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-without-draft', +}); + +// Published rows that still have a draft sibling +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-with-draft', +}); +``` + +`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row). + +## Use with `findOne()` and `findFirst()` {#find-one-find-first} + +`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists. + +```js +await strapi.documents('api::restaurant.restaurant').findOne({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + status: 'draft', + publicationFilter: 'never-published', +}); +``` + +## Combine with `filters` and `populate` {#filters-populate} + +`publicationFilter` is merged with other query filters (logical AND). When [populating relations](/cms/api/document-service/populate), nested queries on draft & publish content-types inherit the same cohort logic so populated results stay consistent with the parent query. + +## Count documents in a cohort {#count} + +```js +const neverPublishedCount = await strapi + .documents('api::restaurant.restaurant') + .count({ + status: 'draft', + publicationFilter: 'never-published', + }); +``` + +Without `publicationFilter`, `count({ status: 'draft' })` still counts every draft row, including drafts whose document already has a published version. Use `publicationFilter: 'never-published'` or `'never-published-document'` to count only never-published cohorts (see [`status` documentation](/cms/api/document-service/status#count)). + +## Validation {#validation} + +Unknown `publicationFilter` values are rejected: + +- Document Service API: throws a validation error. +- REST API: returns HTTP `400`. +- GraphQL: invalid enum values fail at query validation. + +## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} + +The boolean `hasPublishedVersion` parameter is deprecated in favor of `publicationFilter`. Strapi still accepts it on the REST API, GraphQL, and Document Service API and maps it to **document-scoped** modes: + +| `hasPublishedVersion` | Maps to | +| --------------------- | ------- | +| `false` (or string `'false'`) | `never-published-document` | +| `true` (or string `'true'`) | `has-published-version-document` | + +If both `publicationFilter` and `hasPublishedVersion` are passed, `publicationFilter` takes precedence. + +REST and GraphQL examples: [REST API: `publicationFilter`](/cms/api/rest/publication-filter#has-published-version-deprecated), [GraphQL API: `publicationFilter`](/cms/api/graphql#publication-filter). + +## Why not filter on `publishedAt` alone? {#why-not-published-at} + +A single row's `publishedAt` only describes that row. Cohorts such as `never-published`, `has-published-version`, and `modified` require comparing or correlating **two rows** for the same `(documentId, locale)`. `publicationFilter` encodes those rules in one server-side query instead of multiple client round-trips. diff --git a/docusaurus/docs/cms/api/document-service/status.md b/docusaurus/docs/cms/api/document-service/status.md index efb7042581..4a458e61e4 100644 --- a/docusaurus/docs/cms/api/document-service/status.md +++ b/docusaurus/docs/cms/api/document-service/status.md @@ -29,6 +29,8 @@ By default the [Document Service API](/cms/api/document-service) returns the dra Passing `{ status: 'draft' }` to a Document Service API query returns the same results as not passing any `status` parameter. ::: +For derived publication cohorts (never-published, modified, and others), see [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). + ## Get the published version with `findOne()` {#find-one} `findOne()` queries return the draft version of a document by default. @@ -152,7 +154,7 @@ const publishedCount = await strapi.documents("api::restaurant.restaurant").coun :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. -This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. There currently is no way to prevent already published documents from being counted. +This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: ## Create a draft and publish it {#create} diff --git a/docusaurus/docs/cms/api/graphql.md b/docusaurus/docs/cms/api/graphql.md index fd49c12cca..1872075b24 100644 --- a/docusaurus/docs/cms/api/graphql.md +++ b/docusaurus/docs/cms/api/graphql.md @@ -425,6 +425,62 @@ query Query($status: PublicationStatus) { } ``` +### Filter by derived publication cohort {#publication-filter} + +If the [Draft & Publish](/cms/features/draft-and-publish) feature is enabled, you can add a `publicationFilter` argument to built-in collection and single-type queries. The GraphQL plugin exposes the same cohorts as the REST API and Document Service API through the `PublicationFilter` enum. + +Combine `publicationFilter` with `status` the same way as for REST (see [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter#status-combination)). + +When `status` is omitted, GraphQL defaults to `PUBLISHED` before applying `publicationFilter` (same as REST). Example: `restaurants(publicationFilter: MODIFIED)` returns published rows in the modified cohort; use `status: DRAFT` to return draft rows instead. + +Built-in root queries (for example `restaurants`, `restaurants_connection`) pass `publicationFilter` down to populated draft & publish relations on nested fields so relation results match the parent query cohort. + +```graphql title="Example: Fetch never-published draft documents" +query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { + restaurants(status: DRAFT, publicationFilter: NEVER_PUBLISHED) { + documentId + name + publishedAt + } +} +``` + +```graphql title="Example: Fetch published rows without a draft peer" +query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { + restaurants(status: PUBLISHED, publicationFilter: PUBLISHED_WITHOUT_DRAFT) { + documentId + name + publishedAt + } +} +``` + +```graphql title="Example: Modified cohort with default PUBLISHED status" +query Query { + restaurants(publicationFilter: MODIFIED) { + documentId + name + publishedAt + } +} +``` + +Available enum values: + +| GraphQL enum | Document Service / REST value | +| ------------ | ----------------------------- | +| `NEVER_PUBLISHED` | `never-published` | +| `HAS_PUBLISHED_VERSION` | `has-published-version` | +| `MODIFIED` | `modified` | +| `UNMODIFIED` | `unmodified` | +| `NEVER_PUBLISHED_DOCUMENT` | `never-published-document` | +| `HAS_PUBLISHED_VERSION_DOCUMENT` | `has-published-version-document` | +| `PUBLISHED_WITHOUT_DRAFT` | `published-without-draft` | +| `PUBLISHED_WITH_DRAFT` | `published-with-draft` | + +:::note +The deprecated `hasPublishedVersion` boolean argument is still accepted (`true` / `false`) and maps to `NEVER_PUBLISHED_DOCUMENT` / `HAS_PUBLISHED_VERSION_DOCUMENT`. If both `publicationFilter` and `hasPublishedVersion` are provided, `publicationFilter` takes precedence. Prefer `publicationFilter` for new queries. +::: ## Mutations diff --git a/docusaurus/docs/cms/api/rest/parameters.md b/docusaurus/docs/cms/api/rest/parameters.md index d683c6e6a5..175c577228 100644 --- a/docusaurus/docs/cms/api/rest/parameters.md +++ b/docusaurus/docs/cms/api/rest/parameters.md @@ -2,7 +2,7 @@ title: Parameters description: Use API parameters to refine your Strapi REST API queries. sidebar_label: Parameters -next: ./filtering-locale-publication.md +next: ./publication-filter.md tags: - API - Content API @@ -25,6 +25,7 @@ The following API parameters are available: | `filters` | Object | [Filter the response](/cms/api/rest/filters) | | `locale` | String | [Select a locale](/cms/api/rest/locale) | | `status` | String | [Select the Draft & Publish status](/cms/api/rest/status) | +| `publicationFilter` | String | [Select a derived Draft & Publish cohort](/cms/api/rest/publication-filter) | | `populate` | String or Object | [Populate relations, components, or dynamic zones](/cms/api/rest/populate-select#population) | | `fields` | Array | [Select only specific fields to display](/cms/api/rest/populate-select#field-selection) | | `sort` | String or Array | [Sort the response](/cms/api/rest/sort-pagination.md#sorting) | diff --git a/docusaurus/docs/cms/api/rest/publication-filter.md b/docusaurus/docs/cms/api/rest/publication-filter.md new file mode 100644 index 0000000000..9fcc45a9be --- /dev/null +++ b/docusaurus/docs/cms/api/rest/publication-filter.md @@ -0,0 +1,205 @@ +--- +title: Publication filter +description: Use the publicationFilter parameter with Strapi's REST API to query derived Draft & Publish cohorts such as never-published or modified documents. +sidebarDepth: 3 +sidebar_label: Publication filter +displayed_sidebar: cmsSidebar +tags: +- API +- Content API +- find +- interactive query builder +- publicationFilter +- qs library +- REST API +- status +--- + +import QsForQueryBody from '/docs/snippets/qs-for-query-body.md' +import QsForQueryTitle from '/docs/snippets/qs-for-query-title.md' + +# REST API: `publicationFilter` + +The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. It selects derived publication cohorts while [`status`](/cms/api/rest/status) selects draft or published rows. + +:::prerequisites +The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. +::: + +## Default `status` {#default-status} + +When `status` is omitted, the REST API defaults to `status=published` **before** applying `publicationFilter`. + +| Query | Effective behavior | +| ----- | ------------------ | +| `?publicationFilter=never-published` | Empty (cohort is draft-only; default status is `published`) | +| `?status=draft&publicationFilter=never-published` | Never-published draft rows | +| `?publicationFilter=modified` | Published rows in the modified cohort | +| `?status=draft&publicationFilter=modified` | Draft rows in the modified cohort | +| `?publicationFilter=published-without-draft` | Orphan published rows (default `status=published` is correct) | + +The Document Service API defaults to `status=draft` instead. See [Document Service API: default `status`](/cms/api/document-service/publication-filter#default-status). + +Cohort definitions, the full `status` × `publicationFilter` matrix, Content Manager mapping, and validation rules are documented on [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). + +The REST API accepts the same kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. + +Invalid values return HTTP `400`. + +## Get never-published draft documents {#never-published} + +`GET /api/restaurants?status=draft&publicationFilter=never-published` + + + + + +`GET /api/restaurants?status=draft&publicationFilter=never-published` + + + +
+JavaScript query (built with the qs library): + + + +```js +const qs = require('qs'); +const query = qs.stringify( + { + status: 'draft', + publicationFilter: 'never-published', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + +
+ + + +```json {6} +{ + "data": [ + { + "documentId": "a1b2c3d4e5f6g7h8i9j0klm", + "name": "New Restaurant", + "publishedAt": null, + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + + + +
+ +## Get modified documents (default published slice) {#modified} + +With no `status` parameter, REST returns **published** rows in the modified cohort: + +`GET /api/restaurants?publicationFilter=modified` + +To get **draft** rows instead, add `status=draft`: + +`GET /api/restaurants?status=draft&publicationFilter=modified` + + + + + +`GET /api/restaurants?publicationFilter=modified` + + + +
+JavaScript query (built with the qs library): + + + +```js +const qs = require('qs'); +const query = qs.stringify( + { + publicationFilter: 'modified', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + +
+ +
+ +## Get published rows without a draft peer {#published-without-draft} + +`GET /api/restaurants?status=published&publicationFilter=published-without-draft` + +Because REST defaults to `status=published`, `?publicationFilter=published-without-draft` alone is equivalent. + + + + + +`GET /api/restaurants?publicationFilter=published-without-draft` + + + +
+JavaScript query (built with the qs library): + + + +```js +const qs = require('qs'); +const query = qs.stringify( + { + publicationFilter: 'published-without-draft', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + +
+ +
+ +## Combine with other parameters {#combine} + +`publicationFilter` can be combined with [`filters`](/cms/api/rest/filters), [`locale`](/cms/api/rest/locale), [`populate`](/cms/api/rest/populate-select), and other [REST parameters](/cms/api/rest/parameters). All conditions are applied together. + +## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} + +The boolean `hasPublishedVersion` query parameter is deprecated. Accepted values: `true`, `false`, `'true'`, or `'false'`. Strapi maps it to document-scoped `publicationFilter` values: + +| `hasPublishedVersion` | Maps to | +| --------------------- | ------- | +| `false` / `'false'` | `never-published-document` | +| `true` / `'true'` | `has-published-version-document` | + +Example: `GET /api/restaurants?status=draft&hasPublishedVersion=false` + +If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` wins. + +Prefer `publicationFilter` for new integrations. diff --git a/docusaurus/docs/cms/api/rest/status.md b/docusaurus/docs/cms/api/rest/status.md index 2d38ff9a0f..c40ec49572 100644 --- a/docusaurus/docs/cms/api/rest/status.md +++ b/docusaurus/docs/cms/api/rest/status.md @@ -39,6 +39,8 @@ In the response data, the `publishedAt` field is `null` for drafts. Since published versions are returned by default, passing no status parameter is equivalent to passing `status=published`. ::: +For derived publication cohorts (never-published, modified, and others), see [REST API: `publicationFilter`](/cms/api/rest/publication-filter). +

diff --git a/docusaurus/docs/cms/features/draft-and-publish.md b/docusaurus/docs/cms/features/draft-and-publish.md index 9f72034c4e..2a52b18031 100644 --- a/docusaurus/docs/cms/features/draft-and-publish.md +++ b/docusaurus/docs/cms/features/draft-and-publish.md @@ -185,16 +185,19 @@ To unpublish several entries at the same time: ### Usage with APIs -Draft or published content can be requested, created, updated, and deleted using the `status` parameter through the various front-end APIs accessible from [Strapi's Content API](/cms/api/content-api): +Draft or published content can be requested, created, updated, and deleted using the `status` parameter through the various front-end APIs accessible from [Strapi's Content API](/cms/api/content-api). To query derived cohorts such as never-published or modified documents, use the `publicationFilter` parameter (REST and GraphQL) or the equivalent Document Service API option. + + On the back-end server of Strapi, the Document Service API can also be used to interact with localized content: + diff --git a/docusaurus/sidebars.js b/docusaurus/sidebars.js index bc344ead8d..f038c428b3 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -214,6 +214,7 @@ const sidebars = { 'cms/api/rest/filters', 'cms/api/rest/locale', 'cms/api/rest/status', + 'cms/api/rest/publication-filter', 'cms/api/rest/populate-select', 'cms/api/rest/relations', 'cms/api/rest/sort-pagination', @@ -249,6 +250,7 @@ const sidebars = { 'cms/api/document-service/populate', 'cms/api/document-service/sort-pagination', 'cms/api/document-service/status', + 'cms/api/document-service/publication-filter', ], }, ], diff --git a/docusaurus/static/llms-code.txt b/docusaurus/static/llms-code.txt index 6575ecdccc..d4e776e485 100644 --- a/docusaurus/static/llms-code.txt +++ b/docusaurus/static/llms-code.txt @@ -4911,6 +4911,159 @@ File path: N/A +# Using publicationFilter with the Document Service API +Source: https://docs.strapi.io/cms/api/document-service/publication-filter + +## Default status when publicationFilter is used +Description: Example with publicationFilter: 'modified' and no status: +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#default-status) + +Language: JavaScript +File path: N/A + +```js +// Document Service API → draft rows in the modified cohort +await strapi.documents('api::restaurant.restaurant').findMany({ + publicationFilter: 'modified', +}); + +// REST: GET /api/restaurants?publicationFilter=modified → published rows in the modified cohort +``` + + +## Query never-published drafts +Description: Valid but empty combinations do not return validation errors. +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#never-published) + +Language: JavaScript +File path: N/A + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published', +}); +``` + + +## Query has-published-version drafts +Description: Returns draft rows for (documentId, locale) pairs with no published version for that locale. +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#has-published-version) + +Language: JavaScript +File path: N/A + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version', +}); +``` + + +## Query modified or unmodified documents +Description: Returns draft rows where a published row also exists for the same (documentId, locale). +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#modified-unmodified) + +Language: JavaScript +File path: N/A + +```js +// Draft side of modified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'modified', +}); + +// Published side of unmodified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'unmodified', +}); +``` + + +## Query document-scoped cohorts +Description: Comparison uses updatedAt on the draft and published rows for the same pair. +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#document-scoped) + +Language: JavaScript +File path: N/A + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published-document', +}); +``` + +Language: JavaScript +File path: N/A + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version-document', +}); +``` + + +## Query published rows without or with a draft peer +Description: Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped has-published-version). +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#published-slice) + +Language: JavaScript +File path: N/A + +```js +// Orphan published rows (published row, no draft sibling for the same pair) +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-without-draft', +}); + +// Published rows that still have a draft sibling +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-with-draft', +}); +``` + + +## Use with findOne() and findFirst() +Description: publicationFilter applies the same cohort rules. +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#find-one-find-first) + +Language: JavaScript +File path: N/A + +```js +await strapi.documents('api::restaurant.restaurant').findOne({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + status: 'draft', + publicationFilter: 'never-published', +}); +``` + + +## Count documents in a cohort +Description: publicationFilter is merged with other query filters (logical AND). +(Source: https://docs.strapi.io/cms/api/document-service/publication-filter#count) + +Language: JavaScript +File path: N/A + +```js +const neverPublishedCount = await strapi + .documents('api::restaurant.restaurant') + .count({ + status: 'draft', + publicationFilter: 'never-published', + }); +``` + + + # Using Sort & Pagination with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/sort-pagination @@ -6421,6 +6574,52 @@ query Query($status: PublicationStatus) { ``` +## Filter by derived publication cohort +Description: Built-in root queries (for example restaurants, restaurants_connection) pass publicationFilter down to populated draft & publish relations on nested fields so relation results match the parent query cohort. +(Source: https://docs.strapi.io/cms/api/graphql#publication-filter) + +Language: GRAPHQL +File path: Example: + +```graphql +query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { + restaurants(status: DRAFT, publicationFilter: NEVER_PUBLISHED) { + documentId + name + publishedAt + } +} +``` + +--- +Language: GRAPHQL +File path: Example: + +```graphql +query Query($status: PublicationStatus, $publicationFilter: PublicationFilter) { + restaurants(status: PUBLISHED, publicationFilter: PUBLISHED_WITHOUT_DRAFT) { + documentId + name + publishedAt + } +} +``` + +--- +Language: GRAPHQL +File path: Example: + +```graphql +query Query { + restaurants(publicationFilter: MODIFIED) { + documentId + name + publishedAt + } +} +``` + + ## Create a new document Description: The following example creates a new document for the "Restaurant" content-type and returns its name and documentId: (Source: https://docs.strapi.io/cms/api/graphql#create-a-new-document) @@ -10352,6 +10551,102 @@ File path: N/A +# Publication filter +Source: https://docs.strapi.io/cms/api/rest/publication-filter + +## Get never-published draft documents +Description: Code example from "Get never-published draft documents" +(Source: https://docs.strapi.io/cms/api/rest/publication-filter#never-published) + +Language: JavaScript +File path: N/A + +```js +const qs = require('qs'); +const query = qs.stringify( + { + status: 'draft', + publicationFilter: 'never-published', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + +--- +Language: JSON +File path: N/A + +```json +{ + "data": [ + { + "documentId": "a1b2c3d4e5f6g7h8i9j0klm", + "name": "New Restaurant", + "publishedAt": null, + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + + +## Get modified documents (default published slice) +Description: Code example from "Get modified documents (default published slice)" +(Source: https://docs.strapi.io/cms/api/rest/publication-filter#modified) + +Language: JavaScript +File path: N/A + +```js +const qs = require('qs'); +const query = qs.stringify( + { + publicationFilter: 'modified', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + + +## Get published rows without a draft peer +Description: Code example from "Get published rows without a draft peer" +(Source: https://docs.strapi.io/cms/api/rest/publication-filter#published-without-draft) + +Language: JavaScript +File path: N/A + +```js +const qs = require('qs'); +const query = qs.stringify( + { + publicationFilter: 'published-without-draft', + }, + { + encodeValuesOnly: true, + } +); + +await request(`/api/restaurants?${query}`); +``` + + + # Relations Source: https://docs.strapi.io/cms/api/rest/relations diff --git a/docusaurus/static/llms-full.txt b/docusaurus/static/llms-full.txt index ba548c7228..dba1b317b2 100644 --- a/docusaurus/static/llms-full.txt +++ b/docusaurus/static/llms-full.txt @@ -2528,6 +2528,7 @@ Syntax: `findFirst(parameters: Params) => Document` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#find-first) | Locale of the documents to find. | Default locale | String or `undefined` | | [`status`](/cms/api/document-service/status#find-first) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | | [`fields`](/cms/api/document-service/fields#findfirst) | [Select fields](/cms/api/document-service/fields#findfirst) to return | All fields
(except those not populate by default) | Object | | [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | @@ -2562,6 +2563,7 @@ Syntax: `findMany(parameters: Params) => Document[]` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#find-many) | Locale of the documents to find. | Default locale | String or `undefined` | | [`status`](/cms/api/document-service/status#find-many) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | | [`fields`](/cms/api/document-service/fields#findmany) | [Select fields](/cms/api/document-service/fields#findmany) to return | All fields
(except those not populate by default) | Object | | [`populate`](/cms/api/document-service/populate) | [Populate](/cms/api/document-service/populate) results with additional fields. | `null` | Object | @@ -2808,12 +2810,13 @@ Syntax: `count(parameters: Params) => number` |-----------|-------------|---------|------| | [`locale`](/cms/api/document-service/locale#count) | Locale of the documents to count | Default locale | String or `null` | | [`status`](/cms/api/document-service/status#count) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Publication status, can be: | `'draft'` | `'published'` or `'draft'` | +| [`publicationFilter`](/cms/api/document-service/publication-filter) | _If [Draft & Publish](/cms/features/draft-and-publish) is enabled for the content-type_:
Derived publication cohort to match before applying `status` | - | String | | [`filters`](/cms/api/document-service/filters) | [Filters](/cms/api/document-service/filters) to use | `null` | Object | :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. -This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. There currently is no way to prevent already published documents from being counted. +This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: #### Examples @@ -3776,6 +3779,239 @@ To populate while deleting documents: +# Using publicationFilter with the Document Service API +Source: https://docs.strapi.io/cms/api/document-service/publication-filter + +# Document Service API: `publicationFilter` + +The [`status`](/cms/api/document-service/status) parameter selects which **row slice** to read for each document: `draft` rows have `publishedAt: null`, and `published` rows have a non-null `publishedAt`. + +The optional `publicationFilter` parameter selects a **derived publication cohort** first: a set of `(documentId, locale)` pairs (or `documentId` only when [Internationalization (i18n)](/cms/features/internationalization) is disabled) defined by how draft and published rows relate. Strapi then returns the row that matches both the cohort and the resolved `status`. + +:::prerequisites +The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. If Draft & Publish is disabled, `publicationFilter` has no effect. +::: + +`publicationFilter` is supported on `findOne()`, `findFirst()`, `findMany()`, and `count()`. It can be combined with [`filters`](/cms/api/document-service/filters), [`populate`](/cms/api/document-service/populate), and other query parameters. Invalid values raise a validation error. + +## Default `status` when `publicationFilter` is used {#default-status} + +`publicationFilter` is applied **after** `status` is resolved (explicitly or by default). Defaults differ by API surface: + +| API surface | Default `status` when omitted | +| ----------- | ----------------------------- | +| Document Service API (direct) | `'draft'` | +| [REST API](/cms/api/rest/publication-filter) | `'published'` | +| [GraphQL API](/cms/api/graphql#publication-filter) | `PUBLISHED` | + +Example with `publicationFilter: 'modified'` and no `status`: + +```js +// Document Service API → draft rows in the modified cohort +await strapi.documents('api::restaurant.restaurant').findMany({ + publicationFilter: 'modified', +}); + +// REST: GET /api/restaurants?publicationFilter=modified → published rows in the modified cohort +``` + +Pair-scoped modes such as `never-published` only include draft rows in the cohort. With REST or GraphQL defaults (`status=published`), those queries return an empty result set unless you pass `status=draft` / `status: DRAFT`. + +## Available values {#values} + +REST and the Document Service API use kebab-case strings. GraphQL exposes the same cohorts through the [`PublicationFilter` enum](/cms/api/graphql#publication-filter). + +| Value | Scope | Cohort definition (which `(documentId, locale)` pairs match) | +| ----- | ----- | -------------------------------------------------------------- | +| `never-published` | Pair | No row with non-null `publishedAt` exists for the same `(documentId, locale)` | +| `has-published-version` | Pair | **Both** a draft row and a published row exist for the same `(documentId, locale)` | +| `modified` | Pair | Both slices exist and `draft.updatedAt > published.updatedAt` | +| `unmodified` | Pair | Both slices exist and `draft.updatedAt <= published.updatedAt` | +| `never-published-document` | Document | No row with non-null `publishedAt` exists for the same `documentId` in **any** locale | +| `has-published-version-document` | Document | At least one published row exists for the same `documentId` in **any** locale | +| `published-without-draft` | Pair | A published row exists for the pair and **no** draft row exists for the same `(documentId, locale)` | +| `published-with-draft` | Pair | A published row exists for the pair and a draft row **also** exists for the same `(documentId, locale)` | + +For content-types without i18n, read `(documentId, locale)` as `documentId` only. + +### Semantics notes {#semantics} + +- **`has-published-version` excludes orphan published rows**: If only a published row exists for a pair (no draft sibling), that pair is **not** in the `has-published-version` cohort. Orphan published rows can appear under `published-without-draft` when querying with `status: 'published'`. +- **`modified` / `unmodified` require both slices**: Pairs with only a draft or only a published row are not included. +- **`modified` ∪ `unmodified` = `has-published-version`** (for the same `status`): The two modes partition pairs that have both slices. +- **Document-scoped modes**: Existence checks use `documentId` only. A document with draft EN + published NL qualifies for `has-published-version-document` even though EN is never published at the pair level. +- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They are degenerate (empty) with `status: 'draft'`. + +### Content Manager list filters {#content-manager} + +The Content Manager **Status** filter (`__status`) is translated server-side. Only the **Draft (never published)** option uses `publicationFilter`: + +| Content Manager filter | Document Service query equivalent | +| ---------------------- | --------------------------------- | +| Draft (never published) | `status: 'draft'`, `publicationFilter: 'never-published-document'` | +| Published (all) | `status: 'published'` (no `publicationFilter`) | +| Published (modified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter); similar intent to `status: 'published'` + `publicationFilter: 'modified'` but implemented separately in the Content Manager API | +| Published (unmodified) | Internal `publicationStatusFilter` (not a public REST/GraphQL parameter) | + +The **Draft (never published)** filter is document-scoped (`never-published-document`), not pair-scoped `never-published`. + +## Combine `status` and `publicationFilter` {#status-combination} + +| `status` | `publicationFilter` | Rows returned | +| -------- | ------------------- | ------------- | +| `draft` | `never-published` | Draft rows for pairs never published in that locale | +| `published` | `never-published` | Empty (degenerate) | +| `draft` | `has-published-version` | Draft rows for pairs that also have a published version | +| `published` | `has-published-version` | Published rows for pairs that also have a draft version (excludes orphan published-only pairs) | +| `draft` | `modified` | Draft rows newer than their published peer | +| `published` | `modified` | Published rows whose draft peer is newer | +| `draft` | `unmodified` | Draft rows not newer than their published peer | +| `published` | `unmodified` | Published rows whose draft peer is not newer | +| `draft` | `never-published-document` | Draft rows whose document has no published row in any locale | +| `published` | `never-published-document` | Empty (degenerate) | +| `draft` | `has-published-version-document` | Draft rows whose document has at least one published row (any locale) | +| `published` | `has-published-version-document` | Published rows whose document has at least one draft row (any locale) | +| `published` | `published-without-draft` | Published rows with no draft sibling for the same pair | +| `draft` | `published-without-draft` | Empty (degenerate) | +| `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | +| `draft` | `published-with-draft` | Empty (degenerate) | + +Valid but empty combinations do not return validation errors. + +## Query never-published drafts {#never-published} + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published', +}); +``` + +Returns draft rows for `(documentId, locale)` pairs with no published version for that locale. + +## Query has-published-version drafts {#has-published-version} + +```js +const documents = await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version', +}); +``` + +Returns draft rows where a published row also exists for the same `(documentId, locale)`. Does not return draft rows for pairs that only exist as orphan published rows. + +## Query modified or unmodified documents {#modified-unmodified} + +```js +// Draft side of modified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'modified', +}); + +// Published side of unmodified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'unmodified', +}); +``` + +Comparison uses `updatedAt` on the draft and published rows for the same pair. + +## Query document-scoped cohorts {#document-scoped} + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'never-published-document', +}); +``` + +Returns draft rows for documents that have **never** been published in any locale. A multi-locale document with one published locale is excluded entirely, including its draft-only locales. + +```js +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'draft', + publicationFilter: 'has-published-version-document', +}); +``` + +Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped `has-published-version`). + +## Query published rows without or with a draft peer {#published-slice} + +```js +// Orphan published rows (published row, no draft sibling for the same pair) +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-without-draft', +}); + +// Published rows that still have a draft sibling +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'published-with-draft', +}); +``` + +`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row). + +## Use with `findOne()` and `findFirst()` {#find-one-find-first} + +`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists. + +```js +await strapi.documents('api::restaurant.restaurant').findOne({ + documentId: 'a1b2c3d4e5f6g7h8i9j0klm', + status: 'draft', + publicationFilter: 'never-published', +}); +``` + +## Combine with `filters` and `populate` {#filters-populate} + +`publicationFilter` is merged with other query filters (logical AND). When [populating relations](/cms/api/document-service/populate), nested queries on draft & publish content-types inherit the same cohort logic so populated results stay consistent with the parent query. + +## Count documents in a cohort {#count} + +```js +const neverPublishedCount = await strapi + .documents('api::restaurant.restaurant') + .count({ + status: 'draft', + publicationFilter: 'never-published', + }); +``` + +Without `publicationFilter`, `count({ status: 'draft' })` still counts every draft row, including drafts whose document already has a published version. Use `publicationFilter: 'never-published'` or `'never-published-document'` to count only never-published cohorts (see [`status` documentation](/cms/api/document-service/status#count)). + +## Validation {#validation} + +Unknown `publicationFilter` values are rejected: + +- Document Service API: throws a validation error. +- REST API: returns HTTP `400`. +- GraphQL: invalid enum values fail at query validation. + +## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} + +The boolean `hasPublishedVersion` parameter is deprecated in favor of `publicationFilter`. Strapi still accepts it on the REST API, GraphQL, and Document Service API and maps it to **document-scoped** modes: + +| `hasPublishedVersion` | Maps to | +| --------------------- | ------- | +| `false` (or string `'false'`) | `never-published-document` | +| `true` (or string `'true'`) | `has-published-version-document` | + +If both `publicationFilter` and `hasPublishedVersion` are passed, `publicationFilter` takes precedence. + +REST and GraphQL examples: [REST API: `publicationFilter`](/cms/api/rest/publication-filter#has-published-version-deprecated), [GraphQL API: `publicationFilter`](/cms/api/graphql#publication-filter). + +## Why not filter on `publishedAt` alone? {#why-not-published-at} + +A single row's `publishedAt` only describes that row. Cohorts such as `never-published`, `has-published-version`, and `modified` require comparing or correlating **two rows** for the same `(documentId, locale)`. `publicationFilter` encodes those rules in one server-side query instead of multiple client round-trips. + + + # Using Sort & Pagination with the Document Service API Source: https://docs.strapi.io/cms/api/document-service/sort-pagination @@ -3822,6 +4058,8 @@ By default the [Document Service API](/cms/api/document-service) returns the dra Passing `{ status: 'draft' }` to a Document Service API query returns the same results as not passing any `status` parameter. ::: +For derived publication cohorts (never-published, modified, and others), see [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). + ## Get the published version with `findOne()` {#find-one} `findOne()` queries return the draft version of a document by default. @@ -3867,7 +4105,7 @@ const publishedCount = await strapi.documents("api::restaurant.restaurant").coun :::note Since published documents necessarily also have a draft counterpart, a published document is still counted as having a draft version. -This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. There currently is no way to prevent already published documents from being counted. +This means that counting with the `status: 'draft'` parameter still returns the total number of documents matching other parameters, even if some documents have already been published and are not displayed as "draft" or "modified" in the Content Manager anymore. To count only never-published drafts, pass a [`publicationFilter`](/cms/api/document-service/publication-filter) value such as `'never-published'` or `'never-published-document'`. ::: ## Create a draft and publish it {#create} @@ -4515,6 +4753,7 @@ The following API parameters are available: | `filters` | Object | [Filter the response](/cms/api/rest/filters) | | `locale` | String | [Select a locale](/cms/api/rest/locale) | | `status` | String | [Select the Draft & Publish status](/cms/api/rest/status) | +| `publicationFilter` | String | [Select a derived Draft & Publish cohort](/cms/api/rest/publication-filter) | | `populate` | String or Object | [Populate relations, components, or dynamic zones](/cms/api/rest/populate-select#population) | | `fields` | Array | [Select only specific fields to display](/cms/api/rest/populate-select#field-selection) | | `sort` | String or Array | [Sort the response](/cms/api/rest/sort-pagination.md#sorting) | @@ -4608,6 +4847,89 @@ In production, always use explicit population instead of wildcards like `populat +# Publication filter +Source: https://docs.strapi.io/cms/api/rest/publication-filter + +# REST API: `publicationFilter` + +The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. It selects derived publication cohorts while [`status`](/cms/api/rest/status) selects draft or published rows. + +:::prerequisites +The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. +::: + +## Default `status` {#default-status} + +When `status` is omitted, the REST API defaults to `status=published` **before** applying `publicationFilter`. + +| Query | Effective behavior | +| ----- | ------------------ | +| `?publicationFilter=never-published` | Empty (cohort is draft-only; default status is `published`) | +| `?status=draft&publicationFilter=never-published` | Never-published draft rows | +| `?publicationFilter=modified` | Published rows in the modified cohort | +| `?status=draft&publicationFilter=modified` | Draft rows in the modified cohort | +| `?publicationFilter=published-without-draft` | Orphan published rows (default `status=published` is correct) | + +The Document Service API defaults to `status=draft` instead. See [Document Service API: default `status`](/cms/api/document-service/publication-filter#default-status). + +Cohort definitions, the full `status` × `publicationFilter` matrix, Content Manager mapping, and validation rules are documented on [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). + +The REST API accepts the same kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. + +Invalid values return HTTP `400`. + +## Get never-published draft documents {#never-published} + +`GET /api/restaurants?status=draft&publicationFilter=never-published` + +
+JavaScript query (built with the qs library): + + + +## Get modified documents (default published slice) {#modified} + +With no `status` parameter, REST returns **published** rows in the modified cohort: + +`GET /api/restaurants?publicationFilter=modified` + +To get **draft** rows instead, add `status=draft`: + +`GET /api/restaurants?status=draft&publicationFilter=modified` + +
+JavaScript query (built with the qs library): + +## Get published rows without a draft peer {#published-without-draft} + +`GET /api/restaurants?status=published&publicationFilter=published-without-draft` + +Because REST defaults to `status=published`, `?publicationFilter=published-without-draft` alone is equivalent. + +
+JavaScript query (built with the qs library): + +## Combine with other parameters {#combine} + +`publicationFilter` can be combined with [`filters`](/cms/api/rest/filters), [`locale`](/cms/api/rest/locale), [`populate`](/cms/api/rest/populate-select), and other [REST parameters](/cms/api/rest/parameters). All conditions are applied together. + +## Deprecated `hasPublishedVersion` parameter {#has-published-version-deprecated} + +The boolean `hasPublishedVersion` query parameter is deprecated. Accepted values: `true`, `false`, `'true'`, or `'false'`. Strapi maps it to document-scoped `publicationFilter` values: + +| `hasPublishedVersion` | Maps to | +| --------------------- | ------- | +| `false` / `'false'` | `never-published-document` | +| `true` / `'true'` | `has-published-version-document` | + +Example: `GET /api/restaurants?status=draft&hasPublishedVersion=false` + +If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` wins. + +Prefer `publicationFilter` for new integrations. + + + # Relations Source: https://docs.strapi.io/cms/api/rest/relations @@ -4895,6 +5217,8 @@ In the response data, the `publishedAt` field is `null` for drafts. Since published versions are returned by default, passing no status parameter is equivalent to passing `status=published`. ::: +For derived publication cohorts (never-published, modified, and others), see [REST API: `publicationFilter`](/cms/api/rest/publication-filter). +

diff --git a/docusaurus/static/llms.txt b/docusaurus/static/llms.txt index 256af874dc..1871c20003 100644 --- a/docusaurus/static/llms.txt +++ b/docusaurus/static/llms.txt @@ -42,6 +42,7 @@ - [Using the locale parameter with the Document Service API](https://docs.strapi.io/cms/api/document-service/locale): Use Strapi's Document Service API to work with locale versions with your queries. - [Extending the Document Service behavior](https://docs.strapi.io/cms/api/document-service/middlewares): This document provides information about the middlewares in the Document Service API. - [Using Populate with the Document Service API](https://docs.strapi.io/cms/api/document-service/populate): Use Strapi's Document Service API to populate or select some fields. +- [Using publicationFilter with the Document Service API](https://docs.strapi.io/cms/api/document-service/publication-filter): Use the publicationFilter parameter with Strapi's Document Service API to query derived Draft & Publish cohorts such as never-published or modified documents. - [Using Sort & Pagination with the Document Service API](https://docs.strapi.io/cms/api/document-service/sort-pagination): Use Strapi's Document Service API to sort and paginate query results - [Using Draft & Publish with the Document Service API](https://docs.strapi.io/cms/api/document-service/status): Use Strapi's Document Service API to return either the draft or the published version of a document - [GraphQL API](https://docs.strapi.io/cms/api/graphql): import DeepFilteringBlogLink from '/docs/snippets/deep-filtering-blog.md' @@ -53,6 +54,7 @@ - [Locale](https://docs.strapi.io/cms/api/rest/locale): Browse the REST API reference for the locale parameter to take advantage of the Internationalization feature through REST. - [Parameters](https://docs.strapi.io/cms/api/rest/parameters): Use API parameters to refine your Strapi REST API queries. - [Populate and Select](https://docs.strapi.io/cms/api/rest/populate-select): Use the populate parameter to include relations, media fields, components, and dynamic zones in REST API responses. Use the fields parameter to return only specific fields. +- [Publication filter](https://docs.strapi.io/cms/api/rest/publication-filter): Use the publicationFilter parameter with Strapi's REST API to query derived Draft & Publish cohorts such as never-published or modified documents. - [Relations](https://docs.strapi.io/cms/api/rest/relations): Use the REST API to manage the order of relations - [Sort and Pagination](https://docs.strapi.io/cms/api/rest/sort-pagination): Use Strapi's REST API to sort or paginate your data. - [Status](https://docs.strapi.io/cms/api/rest/status): Use Strapi's REST API to work with draft or published versions of your documents. From 1c3b0825c5193713b71d7510dc6464f805e3595b Mon Sep 17 00:00:00 2001 From: Ben Irvin Date: Thu, 18 Jun 2026 15:04:59 +0200 Subject: [PATCH 2/2] Polish publicationFilter docs after review. Fix REST page navigation and example consistency, tighten Document Service prose for readers and llms-code extraction, and regenerate LLM artifacts. --- .../document-service/publication-filter.md | 42 ++++++---- docusaurus/docs/cms/api/rest/parameters.md | 2 +- .../docs/cms/api/rest/publication-filter.md | 78 ++++++++++++++----- docusaurus/docs/cms/api/rest/status.md | 1 + .../docs/cms/features/draft-and-publish.md | 2 +- docusaurus/static/llms-code.txt | 68 +++++++++++++--- docusaurus/static/llms-full.txt | 74 +++++++++--------- 7 files changed, 186 insertions(+), 81 deletions(-) diff --git a/docusaurus/docs/cms/api/document-service/publication-filter.md b/docusaurus/docs/cms/api/document-service/publication-filter.md index ef11cb1ecd..69c7ac56c7 100644 --- a/docusaurus/docs/cms/api/document-service/publication-filter.md +++ b/docusaurus/docs/cms/api/document-service/publication-filter.md @@ -38,7 +38,7 @@ The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled o | [REST API](/cms/api/rest/publication-filter) | `'published'` | | [GraphQL API](/cms/api/graphql#publication-filter) | `PUBLISHED` | -Example with `publicationFilter: 'modified'` and no `status`: +The following example compares Document Service and REST behavior when only `publicationFilter: 'modified'` is passed: ```js // Document Service API → draft rows in the modified cohort @@ -74,7 +74,7 @@ For content-types without i18n, read `(documentId, locale)` as `documentId` only - **`modified` / `unmodified` require both slices**: Pairs with only a draft or only a published row are not included. - **`modified` ∪ `unmodified` = `has-published-version`** (for the same `status`): The two modes partition pairs that have both slices. - **Document-scoped modes**: Existence checks use `documentId` only. A document with draft EN + published NL qualifies for `has-published-version-document` even though EN is never published at the pair level. -- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They are degenerate (empty) with `status: 'draft'`. +- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They return no rows when `status` is `'draft'`. ### Content Manager list filters {#content-manager} @@ -94,7 +94,7 @@ The **Draft (never published)** filter is document-scoped (`never-published-docu | `status` | `publicationFilter` | Rows returned | | -------- | ------------------- | ------------- | | `draft` | `never-published` | Draft rows for pairs never published in that locale | -| `published` | `never-published` | Empty (degenerate) | +| `published` | `never-published` | Empty | | `draft` | `has-published-version` | Draft rows for pairs that also have a published version | | `published` | `has-published-version` | Published rows for pairs that also have a draft version (excludes orphan published-only pairs) | | `draft` | `modified` | Draft rows newer than their published peer | @@ -102,18 +102,22 @@ The **Draft (never published)** filter is document-scoped (`never-published-docu | `draft` | `unmodified` | Draft rows not newer than their published peer | | `published` | `unmodified` | Published rows whose draft peer is not newer | | `draft` | `never-published-document` | Draft rows whose document has no published row in any locale | -| `published` | `never-published-document` | Empty (degenerate) | +| `published` | `never-published-document` | Empty | | `draft` | `has-published-version-document` | Draft rows whose document has at least one published row (any locale) | | `published` | `has-published-version-document` | Published rows whose document has at least one draft row (any locale) | | `published` | `published-without-draft` | Published rows with no draft sibling for the same pair | -| `draft` | `published-without-draft` | Empty (degenerate) | +| `draft` | `published-without-draft` | Empty | | `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | -| `draft` | `published-with-draft` | Empty (degenerate) | +| `draft` | `published-with-draft` | Empty | +:::note Valid but empty combinations do not return validation errors. +::: ## Query never-published drafts {#never-published} +Return draft rows for `(documentId, locale)` pairs with no published version for that locale: + ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -121,10 +125,10 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( }); ``` -Returns draft rows for `(documentId, locale)` pairs with no published version for that locale. - ## Query has-published-version drafts {#has-published-version} +Return draft rows where a published row also exists for the same `(documentId, locale)`. Orphan published-only pairs are excluded: + ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -132,10 +136,10 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( }); ``` -Returns draft rows where a published row also exists for the same `(documentId, locale)`. Does not return draft rows for pairs that only exist as orphan published rows. - ## Query modified or unmodified documents {#modified-unmodified} +Compare `updatedAt` on the draft and published rows for the same pair: + ```js // Draft side of modified pairs await strapi.documents('api::restaurant.restaurant').findMany({ @@ -150,10 +154,10 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Comparison uses `updatedAt` on the draft and published rows for the same pair. - ## Query document-scoped cohorts {#document-scoped} +Return draft rows for documents that have never been published in any locale: + ```js await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -161,7 +165,9 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Returns draft rows for documents that have **never** been published in any locale. A multi-locale document with one published locale is excluded entirely, including its draft-only locales. +A multi-locale document with one published locale is excluded entirely, including its draft-only locales. + +Return draft rows for documents that have at least one published row in any locale: ```js await strapi.documents('api::restaurant.restaurant').findMany({ @@ -170,10 +176,12 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped `has-published-version`). +This is broader than pair-scoped `has-published-version`. ## Query published rows without or with a draft peer {#published-slice} +`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row): + ```js // Orphan published rows (published row, no draft sibling for the same pair) await strapi.documents('api::restaurant.restaurant').findMany({ @@ -188,11 +196,9 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row). - ## Use with `findOne()` and `findFirst()` {#find-one-find-first} -`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists. +`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists: ```js await strapi.documents('api::restaurant.restaurant').findOne({ @@ -208,6 +214,8 @@ await strapi.documents('api::restaurant.restaurant').findOne({ ## Count documents in a cohort {#count} +Count draft rows in the never-published cohort: + ```js const neverPublishedCount = await strapi .documents('api::restaurant.restaurant') diff --git a/docusaurus/docs/cms/api/rest/parameters.md b/docusaurus/docs/cms/api/rest/parameters.md index 175c577228..6e9a7997a2 100644 --- a/docusaurus/docs/cms/api/rest/parameters.md +++ b/docusaurus/docs/cms/api/rest/parameters.md @@ -2,7 +2,7 @@ title: Parameters description: Use API parameters to refine your Strapi REST API queries. sidebar_label: Parameters -next: ./publication-filter.md +next: ./filters.md tags: - API - Content API diff --git a/docusaurus/docs/cms/api/rest/publication-filter.md b/docusaurus/docs/cms/api/rest/publication-filter.md index 9fcc45a9be..0007449f2c 100644 --- a/docusaurus/docs/cms/api/rest/publication-filter.md +++ b/docusaurus/docs/cms/api/rest/publication-filter.md @@ -3,6 +3,7 @@ title: Publication filter description: Use the publicationFilter parameter with Strapi's REST API to query derived Draft & Publish cohorts such as never-published or modified documents. sidebarDepth: 3 sidebar_label: Publication filter +next: ./populate-select.md displayed_sidebar: cmsSidebar tags: - API @@ -16,11 +17,10 @@ tags: --- import QsForQueryBody from '/docs/snippets/qs-for-query-body.md' -import QsForQueryTitle from '/docs/snippets/qs-for-query-title.md' # REST API: `publicationFilter` -The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. It selects derived publication cohorts while [`status`](/cms/api/rest/status) selects draft or published rows. +The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. Use it to query derived publication cohorts such as never-published or modified documents. The [`status`](/cms/api/rest/status) parameter still selects whether each matching document returns its draft or published row. :::prerequisites The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. @@ -40,15 +40,15 @@ When `status` is omitted, the REST API defaults to `status=published` **before** The Document Service API defaults to `status=draft` instead. See [Document Service API: default `status`](/cms/api/document-service/publication-filter#default-status). +:::note Cohort definitions, the full `status` × `publicationFilter` matrix, Content Manager mapping, and validation rules are documented on [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). +::: -The REST API accepts the same kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. - -Invalid values return HTTP `400`. +Accepted kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. Invalid values return HTTP `400`. ## Get never-published draft documents {#never-published} -`GET /api/restaurants?status=draft&publicationFilter=never-published` +Pair-scoped `never-published` only matches draft rows. Pass `status=draft` because REST defaults to `status=published`. @@ -107,15 +107,9 @@ await request(`/api/restaurants?${query}`); -## Get modified documents (default published slice) {#modified} - -With no `status` parameter, REST returns **published** rows in the modified cohort: - -`GET /api/restaurants?publicationFilter=modified` - -To get **draft** rows instead, add `status=draft`: +## Get modified documents {#modified} -`GET /api/restaurants?status=draft&publicationFilter=modified` +The `modified` cohort includes pairs where the draft row is newer than its published peer. With no `status` parameter, REST returns **published** rows from that cohort. Pass `status=draft` to return the draft rows instead. @@ -146,13 +140,36 @@ await request(`/api/restaurants?${query}`);
+ + +```json {6} +{ + "data": [ + { + "documentId": "znrlzntu9ei5onjvwfaalu2v", + "name": "Biscotte Restaurant", + "publishedAt": "2024-03-14T15:40:45.330Z", + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + + + ## Get published rows without a draft peer {#published-without-draft} -`GET /api/restaurants?status=published&publicationFilter=published-without-draft` - -Because REST defaults to `status=published`, `?publicationFilter=published-without-draft` alone is equivalent. +The `published-without-draft` cohort matches published rows that have no draft sibling for the same `(documentId, locale)`. Because REST defaults to `status=published`, you can omit `status` in the query URL. @@ -183,6 +200,31 @@ await request(`/api/restaurants?${query}`);
+ + +```json {6} +{ + "data": [ + { + "documentId": "abcdefghijklmno456", + "name": "Legacy Restaurant", + "publishedAt": "2024-01-10T09:15:00.000Z", + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + + + ## Combine with other parameters {#combine} @@ -200,6 +242,6 @@ The boolean `hasPublishedVersion` query parameter is deprecated. Accepted values Example: `GET /api/restaurants?status=draft&hasPublishedVersion=false` -If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` wins. +If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` takes precedence. Prefer `publicationFilter` for new integrations. diff --git a/docusaurus/docs/cms/api/rest/status.md b/docusaurus/docs/cms/api/rest/status.md index c40ec49572..da148fe0a0 100644 --- a/docusaurus/docs/cms/api/rest/status.md +++ b/docusaurus/docs/cms/api/rest/status.md @@ -3,6 +3,7 @@ title: Status description: Use Strapi's REST API to work with draft or published versions of your documents. sidebarDepth: 3 sidebar_label: Status +next: ./publication-filter.md displayed_sidebar: cmsSidebar tags: - API diff --git a/docusaurus/docs/cms/features/draft-and-publish.md b/docusaurus/docs/cms/features/draft-and-publish.md index 2a52b18031..4e570e5d23 100644 --- a/docusaurus/docs/cms/features/draft-and-publish.md +++ b/docusaurus/docs/cms/features/draft-and-publish.md @@ -194,7 +194,7 @@ Draft or published content can be requested, created, updated, and deleted using -On the back-end server of Strapi, the Document Service API can also be used to interact with localized content: +On the back-end server of Strapi, the Document Service API can also query and manage draft and published content: diff --git a/docusaurus/static/llms-code.txt b/docusaurus/static/llms-code.txt index d4e776e485..16b41558bc 100644 --- a/docusaurus/static/llms-code.txt +++ b/docusaurus/static/llms-code.txt @@ -4915,7 +4915,7 @@ File path: N/A Source: https://docs.strapi.io/cms/api/document-service/publication-filter ## Default status when publicationFilter is used -Description: Example with publicationFilter: 'modified' and no status: +Description: The following example compares Document Service and REST behavior when only publicationFilter: 'modified' is passed: (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#default-status) Language: JavaScript @@ -4932,7 +4932,7 @@ await strapi.documents('api::restaurant.restaurant').findMany({ ## Query never-published drafts -Description: Valid but empty combinations do not return validation errors. +Description: Return draft rows for (documentId, locale) pairs with no published version for that locale: (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#never-published) Language: JavaScript @@ -4947,7 +4947,7 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( ## Query has-published-version drafts -Description: Returns draft rows for (documentId, locale) pairs with no published version for that locale. +Description: Return draft rows where a published row also exists for the same (documentId, locale). (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#has-published-version) Language: JavaScript @@ -4962,7 +4962,7 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( ## Query modified or unmodified documents -Description: Returns draft rows where a published row also exists for the same (documentId, locale). +Description: Compare updatedAt on the draft and published rows for the same pair: (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#modified-unmodified) Language: JavaScript @@ -4984,7 +4984,7 @@ await strapi.documents('api::restaurant.restaurant').findMany({ ## Query document-scoped cohorts -Description: Comparison uses updatedAt on the draft and published rows for the same pair. +Description: Return draft rows for documents that have never been published in any locale: (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#document-scoped) Language: JavaScript @@ -5009,7 +5009,7 @@ await strapi.documents('api::restaurant.restaurant').findMany({ ## Query published rows without or with a draft peer -Description: Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped has-published-version). +Description: published-without-draft and published-with-draft partition published rows per (documentId, locale) (excluding pairs with no published row): (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#published-slice) Language: JavaScript @@ -5047,7 +5047,7 @@ await strapi.documents('api::restaurant.restaurant').findOne({ ## Count documents in a cohort -Description: publicationFilter is merged with other query filters (logical AND). +Description: Count draft rows in the never-published cohort: (Source: https://docs.strapi.io/cms/api/document-service/publication-filter#count) Language: JavaScript @@ -10602,8 +10602,8 @@ File path: N/A ``` -## Get modified documents (default published slice) -Description: Code example from "Get modified documents (default published slice)" +## Get modified documents +Description: Code example from "Get modified documents" (Source: https://docs.strapi.io/cms/api/rest/publication-filter#modified) Language: JavaScript @@ -10623,6 +10623,31 @@ const query = qs.stringify( await request(`/api/restaurants?${query}`); ``` +--- +Language: JSON +File path: N/A + +```json +{ + "data": [ + { + "documentId": "znrlzntu9ei5onjvwfaalu2v", + "name": "Biscotte Restaurant", + "publishedAt": "2024-03-14T15:40:45.330Z", + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + ## Get published rows without a draft peer Description: Code example from "Get published rows without a draft peer" @@ -10645,6 +10670,31 @@ const query = qs.stringify( await request(`/api/restaurants?${query}`); ``` +--- +Language: JSON +File path: N/A + +```json +{ + "data": [ + { + "documentId": "abcdefghijklmno456", + "name": "Legacy Restaurant", + "publishedAt": "2024-01-10T09:15:00.000Z", + "locale": "en" + } + ], + "meta": { + "pagination": { + "page": 1, + "pageSize": 25, + "pageCount": 1, + "total": 1 + } + } +} +``` + # Relations diff --git a/docusaurus/static/llms-full.txt b/docusaurus/static/llms-full.txt index dba1b317b2..8f5c006263 100644 --- a/docusaurus/static/llms-full.txt +++ b/docusaurus/static/llms-full.txt @@ -3804,7 +3804,7 @@ The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled o | [REST API](/cms/api/rest/publication-filter) | `'published'` | | [GraphQL API](/cms/api/graphql#publication-filter) | `PUBLISHED` | -Example with `publicationFilter: 'modified'` and no `status`: +The following example compares Document Service and REST behavior when only `publicationFilter: 'modified'` is passed: ```js // Document Service API → draft rows in the modified cohort @@ -3840,7 +3840,7 @@ For content-types without i18n, read `(documentId, locale)` as `documentId` only - **`modified` / `unmodified` require both slices**: Pairs with only a draft or only a published row are not included. - **`modified` ∪ `unmodified` = `has-published-version`** (for the same `status`): The two modes partition pairs that have both slices. - **Document-scoped modes**: Existence checks use `documentId` only. A document with draft EN + published NL qualifies for `has-published-version-document` even though EN is never published at the pair level. -- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They are degenerate (empty) with `status: 'draft'`. +- **Published-slice diagnostics** (`published-without-draft`, `published-with-draft`): Only select published rows. They return no rows when `status` is `'draft'`. ### Content Manager list filters {#content-manager} @@ -3860,7 +3860,7 @@ The **Draft (never published)** filter is document-scoped (`never-published-docu | `status` | `publicationFilter` | Rows returned | | -------- | ------------------- | ------------- | | `draft` | `never-published` | Draft rows for pairs never published in that locale | -| `published` | `never-published` | Empty (degenerate) | +| `published` | `never-published` | Empty | | `draft` | `has-published-version` | Draft rows for pairs that also have a published version | | `published` | `has-published-version` | Published rows for pairs that also have a draft version (excludes orphan published-only pairs) | | `draft` | `modified` | Draft rows newer than their published peer | @@ -3868,18 +3868,22 @@ The **Draft (never published)** filter is document-scoped (`never-published-docu | `draft` | `unmodified` | Draft rows not newer than their published peer | | `published` | `unmodified` | Published rows whose draft peer is not newer | | `draft` | `never-published-document` | Draft rows whose document has no published row in any locale | -| `published` | `never-published-document` | Empty (degenerate) | +| `published` | `never-published-document` | Empty | | `draft` | `has-published-version-document` | Draft rows whose document has at least one published row (any locale) | | `published` | `has-published-version-document` | Published rows whose document has at least one draft row (any locale) | | `published` | `published-without-draft` | Published rows with no draft sibling for the same pair | -| `draft` | `published-without-draft` | Empty (degenerate) | +| `draft` | `published-without-draft` | Empty | | `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | -| `draft` | `published-with-draft` | Empty (degenerate) | +| `draft` | `published-with-draft` | Empty | +:::note Valid but empty combinations do not return validation errors. +::: ## Query never-published drafts {#never-published} +Return draft rows for `(documentId, locale)` pairs with no published version for that locale: + ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -3887,10 +3891,10 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( }); ``` -Returns draft rows for `(documentId, locale)` pairs with no published version for that locale. - ## Query has-published-version drafts {#has-published-version} +Return draft rows where a published row also exists for the same `(documentId, locale)`. Orphan published-only pairs are excluded: + ```js const documents = await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -3898,10 +3902,10 @@ const documents = await strapi.documents('api::restaurant.restaurant').findMany( }); ``` -Returns draft rows where a published row also exists for the same `(documentId, locale)`. Does not return draft rows for pairs that only exist as orphan published rows. - ## Query modified or unmodified documents {#modified-unmodified} +Compare `updatedAt` on the draft and published rows for the same pair: + ```js // Draft side of modified pairs await strapi.documents('api::restaurant.restaurant').findMany({ @@ -3916,10 +3920,10 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Comparison uses `updatedAt` on the draft and published rows for the same pair. - ## Query document-scoped cohorts {#document-scoped} +Return draft rows for documents that have never been published in any locale: + ```js await strapi.documents('api::restaurant.restaurant').findMany({ status: 'draft', @@ -3927,7 +3931,9 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Returns draft rows for documents that have **never** been published in any locale. A multi-locale document with one published locale is excluded entirely, including its draft-only locales. +A multi-locale document with one published locale is excluded entirely, including its draft-only locales. + +Return draft rows for documents that have at least one published row in any locale: ```js await strapi.documents('api::restaurant.restaurant').findMany({ @@ -3936,10 +3942,12 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -Returns draft rows for documents that have at least one published row in any locale (broader than pair-scoped `has-published-version`). +This is broader than pair-scoped `has-published-version`. ## Query published rows without or with a draft peer {#published-slice} +`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row): + ```js // Orphan published rows (published row, no draft sibling for the same pair) await strapi.documents('api::restaurant.restaurant').findMany({ @@ -3954,11 +3962,9 @@ await strapi.documents('api::restaurant.restaurant').findMany({ }); ``` -`published-without-draft` and `published-with-draft` partition published rows per `(documentId, locale)` (excluding pairs with no published row). - ## Use with `findOne()` and `findFirst()` {#find-one-find-first} -`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists. +`publicationFilter` applies the same cohort rules. If the requested document (and locale, when applicable) is not in the cohort, `findOne()` and `findFirst()` return `null` even when the `documentId` exists: ```js await strapi.documents('api::restaurant.restaurant').findOne({ @@ -3974,6 +3980,8 @@ await strapi.documents('api::restaurant.restaurant').findOne({ ## Count documents in a cohort {#count} +Count draft rows in the never-published cohort: + ```js const neverPublishedCount = await strapi .documents('api::restaurant.restaurant') @@ -4852,7 +4860,7 @@ Source: https://docs.strapi.io/cms/api/rest/publication-filter # REST API: `publicationFilter` -The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. It selects derived publication cohorts while [`status`](/cms/api/rest/status) selects draft or published rows. +The [REST API](/cms/api/rest) accepts an optional `publicationFilter` query parameter when [Draft & Publish](/cms/features/draft-and-publish) is enabled. Use it to query derived publication cohorts such as never-published or modified documents. The [`status`](/cms/api/rest/status) parameter still selects whether each matching document returns its draft or published row. :::prerequisites The [Draft & Publish](/cms/features/draft-and-publish) feature must be enabled on the content-type. @@ -4872,43 +4880,39 @@ When `status` is omitted, the REST API defaults to `status=published` **before** The Document Service API defaults to `status=draft` instead. See [Document Service API: default `status`](/cms/api/document-service/publication-filter#default-status). +:::note Cohort definitions, the full `status` × `publicationFilter` matrix, Content Manager mapping, and validation rules are documented on [Document Service API: `publicationFilter`](/cms/api/document-service/publication-filter). +::: -The REST API accepts the same kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. - -Invalid values return HTTP `400`. +Accepted kebab-case values: `never-published`, `has-published-version`, `modified`, `unmodified`, `never-published-document`, `has-published-version-document`, `published-without-draft`, `published-with-draft`. Invalid values return HTTP `400`. ## Get never-published draft documents {#never-published} -`GET /api/restaurants?status=draft&publicationFilter=never-published` +Pair-scoped `never-published` only matches draft rows. Pass `status=draft` because REST defaults to `status=published`.
JavaScript query (built with the qs library): -## Get modified documents (default published slice) {#modified} - -With no `status` parameter, REST returns **published** rows in the modified cohort: - -`GET /api/restaurants?publicationFilter=modified` - -To get **draft** rows instead, add `status=draft`: +## Get modified documents {#modified} -`GET /api/restaurants?status=draft&publicationFilter=modified` +The `modified` cohort includes pairs where the draft row is newer than its published peer. With no `status` parameter, REST returns **published** rows from that cohort. Pass `status=draft` to return the draft rows instead.
JavaScript query (built with the qs library): -## Get published rows without a draft peer {#published-without-draft} + -`GET /api/restaurants?status=published&publicationFilter=published-without-draft` +## Get published rows without a draft peer {#published-without-draft} -Because REST defaults to `status=published`, `?publicationFilter=published-without-draft` alone is equivalent. +The `published-without-draft` cohort matches published rows that have no draft sibling for the same `(documentId, locale)`. Because REST defaults to `status=published`, you can omit `status` in the query URL.
JavaScript query (built with the qs library): + + ## Combine with other parameters {#combine} `publicationFilter` can be combined with [`filters`](/cms/api/rest/filters), [`locale`](/cms/api/rest/locale), [`populate`](/cms/api/rest/populate-select), and other [REST parameters](/cms/api/rest/parameters). All conditions are applied together. @@ -4924,7 +4928,7 @@ The boolean `hasPublishedVersion` query parameter is deprecated. Accepted values Example: `GET /api/restaurants?status=draft&hasPublishedVersion=false` -If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` wins. +If both `publicationFilter` and `hasPublishedVersion` are sent, `publicationFilter` takes precedence. Prefer `publicationFilter` for new integrations. @@ -9726,7 +9730,7 @@ The Draft & Publish feature allows to manage drafts for your content. -On the back-end server of Strapi, the Document Service API can also be used to interact with localized content: +On the back-end server of Strapi, the Document Service API can also query and manage draft and published content: