From 16a3ea310cc314b869d1ceb8239d5b63802057bd Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 5 Sep 2023 08:51:31 +0200 Subject: [PATCH 1/2] Init Content API RFC --- rfcs/xxxx-v5-content-api.md | 217 ++++++++++++++++++++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 rfcs/xxxx-v5-content-api.md diff --git a/rfcs/xxxx-v5-content-api.md b/rfcs/xxxx-v5-content-api.md new file mode 100644 index 0000000..2da499b --- /dev/null +++ b/rfcs/xxxx-v5-content-api.md @@ -0,0 +1,217 @@ +- Start Date: 2023-09-05 +- RFC PR: (leave this empty) + +# Summary + +V5 is in preparation and will require making changes to the Content APIs (REST & GraphQL). + +# Motivation + +V5 Content Engine requires some breaking changes to the API, It is also the opportunity to simplify the API Response format following user feedbacks. + +> The main goal of this RFC is to propose a simplified and v5 compatible format while offering a smoother migration path with a legacy format. + +# Detailed design + +### Endpoints + +Based on the [Database changes planned](https://github.com/strapi/rfcs/pull/52),we will now mainly work with the `documentId` within the API. + +**Collection Types** + +| Method | Url | Desc | +| -------- | ----------------------------------------------- | ----------------------------------------------------------------------- | +| `GET` | `/api/:contentType`` | Find a list of documents | +| `POST` | `/api/:contentType` | Create a document | +| `GET` | `/api/:contentType/:documentId` | Find a document | +| `PUT` | `/api/:contentType/:documentId` | Update a document | +| `DELETE` | `/api/:contentType/:documentId` | Delete a document | +| `POST` | `/api/:contentType/actions/:action` | Actions on the collection of documents (bulk actions, custom action...) | +| `POST` | `/api/:contentType/:documentId/actions/:action` | Actions on a specific document | + +**Single Types** + +| Method | Url | Desc | +| -------- | --------------------------------- | ------------------------------ | +| `GET` | /api/:contentType | Find document | +| `PUT` | /api/:contentType | Create or Update the document | +| `DELETE` | /api/:contentType | Delete document | +| `POST` | /api/:contentType/actions/:action | Actions on the single document | + +### Add `documentId` + +Introduction of the `documentId` field and renaming of the old `id` to `entryId` or `entityId` + +**Example** + +```tsx +{ + "data": { + "documentId": "clkgylmcc000008lcdd868feh", + "entryId": "clkgylmcc000008lcdd868feh" + }, + "meta": { + "pagination": { + "page": 1, + "pageSize": 10 + } + } +} +``` + +### Introduce a simplified format + +We are planning to introduce a new version of the API response that flattens the structure and removes some of the complex nesting from v4. + +To avoid conflicts between features & Content Type attributes we will keep the `meta` object but completely flatten the `attributes` & `data.attributes` sub paths. + +**Before** + +```json +{ + "data": { + "id": 1, + "attributes": { + "title": "Article A", + "locale": "en", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z", + "publishedAt": "2023-01-01T00:00:00.000Z", + "relation": { + "data": { + "id": 1, + "attributes": { + "name": "Category A", + "locale": "en", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z", + "publishedAt": "2023-01-01T00:00:00.000Z" + } + } + } + } + }, + "meta": { + "pagination": { + "page": 1, + "pageSize": 10 + } + } +} +``` + +**After** + +```json +{ + "data": { + "documentId": "clkgylmcc000008lcdd868feh", + "entryId": "cpmnyztbcc964008lcft812feo", + "title": "Article A", + "relation": { + "documentId": "clkgylw7d000108lc4rw1bb6s", + "entryId": 1, + "name": "Category A", + "meta": { + "locale": "en", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z", + "publishedAt": "2023-01-01T00:00:00.000Z" + } + }, + "meta": { + "locale": "fr", + "createdAt": "2023-01-01T00:00:00.000Z", + "updatedAt": "2023-01-01T00:00:00.000Z", + "publishedAt": "2023-01-01T00:00:00.000Z" + } + }, + "meta": { + "pagination": { + "page": 1, + "pageSize": 10 + } + } +} +``` + +### Separating meta attributes to avoid any current & future name conflicts + +```json +{ + "data": { + "documentId": "clkgylmcc000008lcdd868feh", + "title": "Article A", + "description": "My description", + "meta": { + "locale": "", + "publishedAt": "", + "createdAt": "", + "createdBy": "" + } + } +} +``` + +Pros: + +- No conflicts between user & system attributes +- Clear separation in different objects +- Easier extraction of all meta fields in one go + +Cons + +- Access is nested. +- Lesser discoverability (need to know it’s in meta) ⇒ the _SDK and/or Typescript typings would make the discoverability issue irrelevant_ + +```tsx +const locale = data.meta.locale; +const { locale } = data.meta; + +// better separation +const { meta, ...attributes } = data; +``` + +### Low level **relational support** + +In order to keep the old relational system, we will return the `entryId` to allow managing low level relations. + +```json +{ + "data": { + "id": "clkgylmcc000008lcdd868feh", + "entryId": 1, + "title": "..." + } +} +``` + +# Release plan + +Doing theses changes would break all the existing API Calls. + +In order to reduce migration cost we are considering support a _deprecated_ response format: + +- Keep the "id" name for the entry id +- Support fetching the API with the old IDs +- Keep the nested `data.attributes` format of v4 + +```tsx +{ + "data": { + "documentId": "clkgylmcc000008lcdd868feh" + "id": "clkgylmcc000008lcdd868feh", + "attributes": {}, + } +} +``` + +- This format could be configured to be the default one during migration. +- We would support a query parameter to switch to the new format incrementally. + +> The breaking changes are still forcing users to switch from calling the api with `id` to `documentId` + +# Unresolved questions + +- Should we use `id` or `documentId` in the new response format. +- Should we use `entryId` or `entityId` to represent the underlying Ids to use for low level relations From 6fabcb95827462b654db1677562b66c0c7553080 Mon Sep 17 00:00:00 2001 From: Alexandre Bodin Date: Tue, 19 Sep 2023 09:26:30 +0200 Subject: [PATCH 2/2] Update RFC --- rfcs/xxxx-v5-content-api.md | 323 ++++++++++++++++++++++++------------ 1 file changed, 214 insertions(+), 109 deletions(-) diff --git a/rfcs/xxxx-v5-content-api.md b/rfcs/xxxx-v5-content-api.md index 2da499b..778877c 100644 --- a/rfcs/xxxx-v5-content-api.md +++ b/rfcs/xxxx-v5-content-api.md @@ -9,47 +9,42 @@ V5 is in preparation and will require making changes to the Content APIs (REST & V5 Content Engine requires some breaking changes to the API, It is also the opportunity to simplify the API Response format following user feedbacks. -> The main goal of this RFC is to propose a simplified and v5 compatible format while offering a smoother migration path with a legacy format. +> The main point of this RFC is to propose a simplified and v5 compatible format while offering a smoother migration path with a legacy format. -# Detailed design +# Detailed proposal -### Endpoints +## Use the internal `documentId` as the only identifier the ContentAPI knows about. -Based on the [Database changes planned](https://github.com/strapi/rfcs/pull/52),we will now mainly work with the `documentId` within the API. +- We will name it `id` for simplicity sake. **Collection Types** -| Method | Url | Desc | -| -------- | ----------------------------------------------- | ----------------------------------------------------------------------- | -| `GET` | `/api/:contentType`` | Find a list of documents | -| `POST` | `/api/:contentType` | Create a document | -| `GET` | `/api/:contentType/:documentId` | Find a document | -| `PUT` | `/api/:contentType/:documentId` | Update a document | -| `DELETE` | `/api/:contentType/:documentId` | Delete a document | -| `POST` | `/api/:contentType/actions/:action` | Actions on the collection of documents (bulk actions, custom action...) | -| `POST` | `/api/:contentType/:documentId/actions/:action` | Actions on a specific document | +| Method | Url | Desc | +| ------ | --------------------------------------- | ----------------------------------------------------------------------- | +| GET | `/api/:contentType` | Find a list of documents | +| POST | `/api/:contentType` | Create a document | +| GET | `/api/:contentType/:id` | Find a document | +| PUT | `/api/:contentType/:id` | Update a document | +| DELETE | `/api/:contentType/:id` | Delete a document | +| POST | `/api/:contentType/actions/:action` | Actions on the collection of documents (bulk actions, custom action...) | +| POST | `/api/:contentType/:id/actions/:action` | Actions on a specific document | **Single Types** -| Method | Url | Desc | -| -------- | --------------------------------- | ------------------------------ | -| `GET` | /api/:contentType | Find document | -| `PUT` | /api/:contentType | Create or Update the document | -| `DELETE` | /api/:contentType | Delete document | -| `POST` | /api/:contentType/actions/:action | Actions on the single document | +| Method | Url | Desc | +| ------ | ----------------------------------- | ------------------------------ | +| GET | `/api/:contentType` | Find document | +| PUT | `/api/:contentType` | Set / Update document | +| DELETE | `/api/:contentType` | Delete document | +| POST | `/api/:contentType/actions/:action` | Actions on the single document | -### Add `documentId` - -Introduction of the `documentId` field and renaming of the old `id` to `entryId` or `entityId` - -**Example** - -```tsx +```json { - "data": { - "documentId": "clkgylmcc000008lcdd868feh", - "entryId": "clkgylmcc000008lcdd868feh" - }, + "data": [ + { + "id": "clkgylmcc000008lcdd868feh" + } + ], "meta": { "pagination": { "page": 1, @@ -59,36 +54,21 @@ Introduction of the `documentId` field and renaming of the old `id` to `entryId` } ``` -### Introduce a simplified format +## Introduce a simplified format -We are planning to introduce a new version of the API response that flattens the structure and removes some of the complex nesting from v4. +The second change is the introduction of a new version of the API response format that flattens the structure. -To avoid conflicts between features & Content Type attributes we will keep the `meta` object but completely flatten the `attributes` & `data.attributes` sub paths. - -**Before** +- No more `data.attributes` ```json { "data": { - "id": 1, - "attributes": { - "title": "Article A", - "locale": "en", - "createdAt": "2023-01-01T00:00:00.000Z", - "updatedAt": "2023-01-01T00:00:00.000Z", - "publishedAt": "2023-01-01T00:00:00.000Z", - "relation": { - "data": { - "id": 1, - "attributes": { - "name": "Category A", - "locale": "en", - "createdAt": "2023-01-01T00:00:00.000Z", - "updatedAt": "2023-01-01T00:00:00.000Z", - "publishedAt": "2023-01-01T00:00:00.000Z" - } - } - } + "id": "clkgylmcc000008lcdd868feh", + "entryId": 1, + "title": "Article A", + "relation": { + "id": "clkgylw7d000108lc4rw1bb6s", + "name": "Category A" } }, "meta": { @@ -100,54 +80,66 @@ To avoid conflicts between features & Content Type attributes we will keep the ` } ``` -**After** +Simplifying this so much will have some downsides that can’t be obvious considering we haven’t used any of the advantages of the v4 format: + +- This new format will not allow relation pagination without adding some nesting + +### Dealing with Strapi attributes vs user attributes + +### Option 1 ```json { "data": { - "documentId": "clkgylmcc000008lcdd868feh", - "entryId": "cpmnyztbcc964008lcft812feo", + "id": "clkgylmcc000008lcdd868feh", "title": "Article A", - "relation": { - "documentId": "clkgylw7d000108lc4rw1bb6s", - "entryId": 1, - "name": "Category A", - "meta": { - "locale": "en", - "createdAt": "2023-01-01T00:00:00.000Z", - "updatedAt": "2023-01-01T00:00:00.000Z", - "publishedAt": "2023-01-01T00:00:00.000Z" - } - }, - "meta": { - "locale": "fr", - "createdAt": "2023-01-01T00:00:00.000Z", - "updatedAt": "2023-01-01T00:00:00.000Z", - "publishedAt": "2023-01-01T00:00:00.000Z" - } - }, - "meta": { - "pagination": { - "page": 1, - "pageSize": 10 - } + "description": "My description", + "locale": "", + "localizations": [], + "publishedAt": "", + "release": "", + "publishedBy": "", + "createdAt": "", + "createdBy": "", + "updatedAt": "", + "updatedBy": "" } } ``` -### Separating meta attributes to avoid any current & future name conflicts +Pros: + +- The simplest API a user can consume. everything is at the root level the user just uses plain data +- Simpler API schema & GraphQL schema +- Same behaviour but with good validation as in v4 so easier migration as it would contain the same attributes as in v4 `data.attrbitues` +- Including the attributes visually in the CTB when creating a content-type could be a good way to show they are internals but are part of the content-type and explains why you can’t use them. + +Cons + +- We prevent the use of specific attribute names ⇒ **One could argue it is even better as it will avoid confusions better than allowing for example: `internals.locale` & `locale` to exist at the same time from Strapi & a user attribute** +- Adding new internal attributes could conflict with existing user attributes ⇒ + **2 options:** + - We release major versions more often for these use-cases & add a deprecation warning before that for users to rename some properties. + - We provide good migration paths for attributes (difficult & error prone + need manual labor from users) + +### Option 2 ```json { "data": { - "documentId": "clkgylmcc000008lcdd868feh", + "id": "clkgylmcc000008lcdd868feh", "title": "Article A", "description": "My description", - "meta": { + "internals": { "locale": "", + "localizations": [], "publishedAt": "", + "release": "", + "publishedBy": "", "createdAt": "", - "createdBy": "" + "createdBy": "", + "updatedAt": "", + "updatedBy": "" } } } @@ -157,61 +149,174 @@ Pros: - No conflicts between user & system attributes - Clear separation in different objects -- Easier extraction of all meta fields in one go +- Easier extraction of all `internal` fields in one go Cons - Access is nested. -- Lesser discoverability (need to know it’s in meta) ⇒ the _SDK and/or Typescript typings would make the discoverability issue irrelevant_ -```tsx -const locale = data.meta.locale; -const { locale } = data.meta; +```js +const locale = data.internals.locale; +const { locale } = data.internals; // better separation -const { meta, ...attributes } = data; +const { internals, ...attributes } = data; ``` -### Low level **relational support** +- Confusion can happen between internal attribute and user attributes if we allow them to have the same names +- Lesser discoverability (need to know it’s in internals) ⇒ the _SDK and/or Typescript typings would make the discoverability issue irrelevant_ +- This makes the DB & Entity service layers more complex -In order to keep the old relational system, we will return the `entryId` to allow managing low level relations. +```js +entityService.find({ + filters: { + locale: 'fr' // Would apply to the user attributes so we need a translation to something else. + internal_locale: 'fr' + }, + populate: { + createdBy: true // same question and conflicts with properties + } +}) +// same issue for Database queries +``` + +### Option 3 + +The main alternative to consider is for meta attributes ```json { "data": { "id": "clkgylmcc000008lcdd868feh", - "entryId": 1, - "title": "..." + "title": "Article A", + "description": "My description", + + "$locale": "", + "$publishedAt": "", + + "_availableLocales": ["en", "fr"], + "_createdAt": "1970-01-01T00:00:00.000Z" } } ``` -# Release plan +Pros: -Doing theses changes would break all the existing API Calls. +- As flat as it gets +- Simple path access +- No conflicts between user & system attributes. But do we really want to allow that ? -In order to reduce migration cost we are considering support a _deprecated_ response format: +```json +{ + "data": { + "id": "kaozdianzoind", + "locale": "myCustomAttributeLocale" + "_locale": "fr", + } +} +``` + +```js +// if so we also need the DB & Entity Service layers to accept this + +entityService.find({ + filters: { + locale: "heyo", + _locale: "fr", + }, +}); +``` + +Cons + +- `_` has been long used for private fields and some eslint rules disallow it (https://eslint.org/docs/latest/rules/no-underscore-dangle). This can make destructuring a bit more annoying. ⇒ Using another special char would fix it and avoid any confusion with private fields (e.g `$`) +- No way to get all metas in one go without mapping over all fields and extracting based on prefix + +```js +const { $locale } = document; +document.$locale + +const meta = Object.entries(document).reduce((res, [key,value]) => key.startsWith('$') ? Object.assign(res, [key]: value) : res,{}); +``` + +- Confusion can happen between internal attribute and user attributes if we allow them to have the same names +- _SDK & Typescript typings wouldn’t improve the cons that much except by adding more API surface for accessing the attributes_ + +We could consider using a more obvious prefix like `strapi` to reduce the potential confusions + +```json +{ + "data": { + "id": "kaozdianzoind", + "locale": "myCustomAttributeLocale", + "strapiLocale": "fr" + } +} +``` + +```js +// if so we also need the DB & Entity Service layers to accept this + +entityService.find({ + filters: { + locale: "heyo", + strapiLocale: "fr", + strapiCreatedBy: "...", + }, +}); +``` + +> This becomes less usable than option 2 + +## Metadata -- Keep the "id" name for the entry id -- Support fetching the API with the old IDs -- Keep the nested `data.attributes` format of v4 +The API can also return data that isn’t directly stored inside the document but attached to it. those metadata can be added in a sub `meta` key that will be used as. -```tsx +```json +{ + "data": { + "id": "clkgylmcc000008lcdd868feh", + "title": "Article A", + "description": "My description", + "meta": { + "localizations": {}, + "publicationState": "", + "reviewWorkflow": {}, + "history": {} + } + } +} +``` + +`Metadata` are added information that you can’t work with directly through the main `content API` endpoints & don’t work the same way as simple attributes that you can select/filter/orderBy + +### Low level **relational support** + +In order to keep the old relational system, we will return the `entryId` to allow managing low level relations. + +```json { "data": { - "documentId": "clkgylmcc000008lcdd868feh" "id": "clkgylmcc000008lcdd868feh", - "attributes": {}, + "entryId": 1, + "title": "..." } } ``` -- This format could be configured to be the default one during migration. -- We would support a query parameter to switch to the new format incrementally. +### Release plan -> The breaking changes are still forcing users to switch from calling the api with `id` to `documentId` +Doing theses changes will break all existing clients application. In order to reduce migration cost we can offer an older version of the ContentAPI response format to ease the transition -# Unresolved questions +```json +{ + "data": { + "id": "entryId", + "_documentId": "azodapzd", + "attributes": {} + } +} +``` -- Should we use `id` or `documentId` in the new response format. -- Should we use `entryId` or `entityId` to represent the underlying Ids to use for low level relations +- We can add a config to define the default format +- Add a query parameter to allow switch to the other format when needed to enable incremental migrations