diff --git a/docusaurus/docs/cms/api/document-service.md b/docusaurus/docs/cms/api/document-service.md index 0d487a5d63..4c6da2a301 100644 --- a/docusaurus/docs/cms/api/document-service.md +++ b/docusaurus/docs/cms/api/document-service.md @@ -109,6 +109,7 @@ Syntax: `findOne(parameters: Params) => Document` { name: 'documentId', type: 'ID', required: true, description: 'Document id' }, { name: 'locale', type: 'String or undefined', required: false, description: 'Locale of the document to find. Defaults to the default locale. See locale docs.' }, { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'publicationFilter', type: 'String', required: false, description: 'If Draft & Publish is enabled: derived publication cohort to match before applying status. See publicationFilter docs.' }, { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, ]} @@ -147,6 +148,7 @@ Syntax: `findFirst(parameters: Params) => Document` params={[ { name: 'locale', type: 'String or undefined', required: false, description: 'Locale of the documents to find. Defaults to the default locale. See locale docs.' }, { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'publicationFilter', type: 'String', required: false, description: 'If Draft & Publish is enabled: derived publication cohort to match before applying status. See publicationFilter docs.' }, { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, @@ -210,6 +212,7 @@ Syntax: `findMany(parameters: Params) => Document[]` params={[ { name: 'locale', type: 'String or undefined', required: false, description: 'Locale of the documents to find. Defaults to the default locale. See locale docs.' }, { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. Can be published or draft. Default: draft. See status docs.' }, + { name: 'publicationFilter', type: 'String', required: false, description: 'If Draft & Publish is enabled: derived publication cohort to match before applying status. See publicationFilter docs.' }, { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, { name: 'fields', type: 'Object', required: false, description: 'Select fields to return. Defaults to all fields (except those not populated by default).' }, { name: 'populate', type: 'Object', required: false, description: 'Populate results with additional fields. Default: null.' }, @@ -606,6 +609,7 @@ Syntax: `count(parameters: Params) => number` params={[ { name: 'locale', type: 'String or null', required: false, description: 'Locale of the documents to count. Defaults to the default locale. See locale docs.' }, { name: 'status', type: "'published' | 'draft'", required: false, description: 'If Draft & Publish is enabled: publication status. published to count only published documents, draft to count draft documents (returns all documents). Default: draft. See status docs.' }, + { name: 'publicationFilter', type: 'String', required: false, description: 'If Draft & Publish is enabled: derived publication cohort to match before applying status. See publicationFilter docs.' }, { name: 'filters', type: 'Object', required: false, description: 'Filters to use. Default: null.' }, ]} codeTabs={[ @@ -633,7 +637,7 @@ strapi.documents('api::restaurant.restaurant').count({ filters: { name: { $start :::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'`. ::: 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..69c7ac56c7 --- /dev/null +++ b/docusaurus/docs/cms/api/document-service/publication-filter.md @@ -0,0 +1,253 @@ +--- +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` | + +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 +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 return no rows when `status` is `'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 | +| `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 | +| `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 | +| `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | +| `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', + publicationFilter: 'never-published', +}); +``` + +## 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', + publicationFilter: 'has-published-version', +}); +``` + +## 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({ + status: 'draft', + publicationFilter: 'modified', +}); + +// Published side of unmodified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'unmodified', +}); +``` + +## 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', + publicationFilter: 'never-published-document', +}); +``` + +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({ + status: 'draft', + publicationFilter: 'has-published-version-document', +}); +``` + +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({ + 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()` {#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} + +Count draft rows in the never-published cohort: + +```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 a7d82ec2e4..d954ff6f7e 100644 --- a/docusaurus/docs/cms/api/document-service/status.md +++ b/docusaurus/docs/cms/api/document-service/status.md @@ -36,6 +36,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} + + + +`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 {#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. + + + + + +`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}`); +``` + +
+ + + +```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} + +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. + + + + + +`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}`); +``` + +
+ + + +```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} + +`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` 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 14f6a693e3..2427970aab 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 @@ -46,6 +47,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). +

+ + -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/sidebars.js b/docusaurus/sidebars.js index 1b257ab057..b522c08805 100644 --- a/docusaurus/sidebars.js +++ b/docusaurus/sidebars.js @@ -202,6 +202,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', @@ -234,6 +235,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 d26aaa78da..43c82d9f55 100644 --- a/docusaurus/static/llms-code.txt +++ b/docusaurus/static/llms-code.txt @@ -4560,6 +4560,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: 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 +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: 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 +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: 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 +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: 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 +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: 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 +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: 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 +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: Count draft rows in the never-published cohort: +(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 @@ -6053,6 +6206,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) @@ -10010,6 +10209,152 @@ await request(`/api/articles?${query}`); +# 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 +Description: Code example from "Get modified documents" +(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}`); +``` + +--- +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" +(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}`); +``` + +--- +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 Source: https://docs.strapi.io/cms/api/rest/relations diff --git a/docusaurus/static/llms-full.txt b/docusaurus/static/llms-full.txt index d8920cf35e..8da332c8a3 100644 --- a/docusaurus/static/llms-full.txt +++ b/docusaurus/static/llms-full.txt @@ -4675,6 +4675,7 @@ Find a document matching the passed documentId and parameters. If only a documen - `documentId` (ID, required): Document id - `locale` (String or undefined): Locale of the document to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-one). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-one). +- `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `fields` (Object): Select fields (/cms/api/document-service/fields#findone) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. @@ -4706,6 +4707,7 @@ Find the first document matching the parameters. By default, findFirst() returns **Parameters:** - `locale` (String or undefined): Locale of the documents to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-first). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-first). +- `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#findfirst) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. @@ -4761,6 +4763,7 @@ Find documents matching the parameters. When no parameter is passed, findMany() **Parameters:** - `locale` (String or undefined): Locale of the documents to find. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#find-many). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. Can be `published` or `draft`. Default: `draft`. See status docs (/cms/api/document-service/status#find-many). +- `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. - `fields` (Object): Select fields (/cms/api/document-service/fields#findmany) to return. Defaults to all fields (except those not populated by default). - `populate` (Object): Populate (/cms/api/document-service/populate) results with additional fields. Default: `null`. @@ -5096,6 +5099,7 @@ Count how many documents match the parameters. If no parameter is passed, the co **Parameters:** - `locale` (String or null): Locale of the documents to count. Defaults to the default locale. See locale docs (/cms/api/document-service/locale#count). - `status` (): If Draft & Publish (/cms/features/draft-and-publish) is enabled: publication status. `published` to count only published documents, `draft` to count draft documents (returns all documents). Default: `draft`. See status docs (/cms/api/document-service/status#count). +- `publicationFilter` (String): If Draft & Publish (/cms/features/draft-and-publish) is enabled: derived publication cohort to match before applying `status`. See publicationFilter docs (/cms/api/document-service/publication-filter). - `filters` (Object): Filters (/cms/api/document-service/filters) to use. Default: `null`. **Generic example:** @@ -5121,7 +5125,7 @@ strapi.documents('api::restaurant.restaurant').count({ filters: { name: { $start :::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'`. ::: @@ -7050,6 +7054,247 @@ strapi.documents("api::article.article").delete({ +# 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` | + +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 +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 return no rows when `status` is `'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 | +| `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 | +| `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 | +| `published` | `published-with-draft` | Published rows that have a draft sibling for the same pair | +| `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', + publicationFilter: 'never-published', +}); +``` + +## 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', + publicationFilter: 'has-published-version', +}); +``` + +## 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({ + status: 'draft', + publicationFilter: 'modified', +}); + +// Published side of unmodified pairs +await strapi.documents('api::restaurant.restaurant').findMany({ + status: 'published', + publicationFilter: 'unmodified', +}); +``` + +## 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', + publicationFilter: 'never-published-document', +}); +``` + +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({ + status: 'draft', + publicationFilter: 'has-published-version-document', +}); +``` + +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({ + 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()` {#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} + +Count draft rows in the never-published cohort: + +```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 @@ -7176,6 +7421,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} #### GET strapi.documents().findOne() — findOne() with status: @@ -7273,7 +7520,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} @@ -8646,6 +8893,63 @@ 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 Mutations in GraphQL are used to modify data (e.g. create, update, and delete data). @@ -13564,6 +13868,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) | @@ -13841,6 +14146,145 @@ 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. 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. +::: + +## 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). + +:::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). +::: + +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} + +Pair-scoped `never-published` only matches draft rows. Pass `status=draft` because REST defaults to `status=published`. + +**Get draft restaurants that have never been published for their locale:** +`GET /api/restaurants?status=draft&publicationFilter=never-published` + +**Example response:** +```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 {#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. + +**Get published restaurants in the modified cohort (default status):** +`GET /api/restaurants?publicationFilter=modified` + +**Example response:** +```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} + +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. + +**Get published restaurants with no draft row for the same locale:** +`GET /api/restaurants?publicationFilter=published-without-draft` + +**Example response:** +```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} + +`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` takes precedence. + +Prefer `publicationFilter` for new integrations. + + + # Relations Source: https://docs.strapi.io/cms/api/rest/relations @@ -14539,6 +14983,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). +

#### GET /api/articles?status=draft — Get draft versions of restaurants @@ -32099,14 +32545,17 @@ 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. - [REST API](/cms/api/rest/status): Learn how to use the status parameter with the REST API. +- [REST API: publicationFilter](/cms/api/rest/publication-filter): Query never-published, modified, and other derived publication cohorts. - [GraphQL API](/cms/api/graphql#status): Learn how to use the status parameter with GraphQL API. +- [GraphQL API: publicationFilter](/cms/api/graphql#publication-filter): Query derived publication cohorts with the PublicationFilter enum. -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: - [Document Service API](/cms/api/document-service/status): Learn how to use the status parameter with the Document Service API. +- [Document Service API: publicationFilter](/cms/api/document-service/publication-filter): Query derived publication cohorts from server-side code. diff --git a/docusaurus/static/llms.txt b/docusaurus/static/llms.txt index a0ce549e4a..1317221176 100644 --- a/docusaurus/static/llms.txt +++ b/docusaurus/static/llms.txt @@ -44,6 +44,7 @@ - [Using the locale parameter with the Document Service API](https://docs.strapi.io/cms/api/document-service/locale.md): The locale parameter in the Document Service API lets you query, create, update, delete, publish, and unpublish documents for specific language versions using methods like findOne(), findMany(), update(), and delete(). - [Extending the Document Service behavior](https://docs.strapi.io/cms/api/document-service/middlewares.md): Document Service middlewares allow you to perform actions before and after Document Service methods run by registering middleware functions via strapi.documents.use() with access to content type context and method parameters. - [Using Populate with the Document Service API](https://docs.strapi.io/cms/api/document-service/populate.md): Use the populate parameter with the Document Service API to explicitly load relations, media fields, components, and dynamic zones at one or multiple levels deep, and within create(), update(), publish(), and delete() operations. +- [Using publicationFilter with the Document Service API](https://docs.strapi.io/cms/api/document-service/publication-filter.md): 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.md): Use the Document Service API's sort and pagination parameters to order query results by single or multiple fields and control result limits with limit and start. - [Using Draft & Publish with the Document Service API](https://docs.strapi.io/cms/api/document-service/status.md): Use the status parameter with the Document Service API to retrieve published or draft versions of documents, count documents by status, and directly publish documents during creation or updates. - [Entity Service API](https://docs.strapi.io/cms/api/entity-service.md): The Entity Service API is a backend layer that handles complex content structures like components and dynamic zones, providing CRUD operations, filtering, populating relations, and pagination via strapi.entityService. @@ -72,6 +73,7 @@ - [Locale](https://docs.strapi.io/cms/api/rest/locale.md): The locale REST API parameter retrieves and manages documents in specific languages, defaulting to the application's default locale. Use it to fetch, create, update, and delete locale-specific versions of documents in both collection and single types. - [Parameters](https://docs.strapi.io/cms/api/rest/parameters.md): REST API parameters filter, sort, paginate, and select fields and relations in Strapi queries. Use filters, locale, populate, sort, and pagination to refine your content requests. - [Populate and Select](https://docs.strapi.io/cms/api/rest/populate-select.md): 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.md): 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.md): Use connect, disconnect, and set parameters in REST and GraphQL API requests to manage relations between content-types. Reorder relations using positional arguments like before, after, start, or end. - [Sort and Pagination](https://docs.strapi.io/cms/api/rest/sort-pagination.md): Sort REST API results on one or multiple fields with :asc or :desc syntax, and paginate using either page-based or offset-based parameters. - [Status](https://docs.strapi.io/cms/api/rest/status.md): The REST API's status parameter filters documents by their publication state, returning either published versions (default) or drafts by passing status=draft.