feat: translation flows, revision conflicts, delete scopes, article moves and a target site for new articles

This commit is contained in:
Florian Sylvain committed 2026-11-03 00:12:29 +01:00
1 parent 339e646eb8
commit 45fbbc33b1
93 files changed
+7630 -294

No files matched your search

+17 -6
View File
@@ -28,25 +28,25 @@ The headers are set on those responses only. Same-origin SPA code reads them dir
| --- | --- | --- |
| 400 | `The request cannot be processed due to a mismatch in the format of the body.` | undecodable or invalid body, missing `copy`, `copy: false` with a source, `if_revision` invalid, an unknown or empty update |
| 400 | `The server expects the ID to be in the format of an unsigned 32-bit integer (uint32).` | invalid id in the path |
| 400 | the validator text (for example `Key: 'PostTranslation.Title' Error:Field validation for 'Title' failed on the 'min' tag`) | `lang` or `title` missing, title shorter than 3 or longer than 200 characters on a create |
| 400 | the validator text (for example `Key: 'PostTranslation.Title' Error:Field validation for 'Title' failed on the 'min' tag`) | `lang` or `title` missing, title shorter than 3 or longer than 200 characters on a create, `site` that is `""` or not a site key on `POST /v1/article/` |
| 400 | `Article title must be between 3 and 200 characters.` | a `PATCH` title outside 3 to 200 runes |
| 400 | `invalid_slug` | a `PATCH` slug that is not a slug |
| 401 | `You must be logged in to access this resource.` | no valid session or key |
| 403 | `missing_permission` | the caller lacks the permission (`X-Missing-Permission`) |
| 404 | `Article not found` | the version, or the story, does not exist or the caller cannot read it |
| 404 | `source_unavailable` | the chosen source does not exist or is not readable; the same answer for both |
| 404 | `site_not_found` | `POST /v1/story/{story_id}/move` with an unknown site key |
| 404 | `site_not_found` | `POST /v1/story/{story_id}/move`, or `POST /v1/article/` with `site`, naming an unknown site key |
| 409 | `revision_conflict` | `if_revision` is not the current `revision` (`X-Current-Revision`) |
| 409 | `language_exists` | the language already has a readable version (`X-Existing-Article-Id`) |
| 409 | `language_unavailable` | one neutral answer: the language is disabled on the site, or its version exists but is not readable by the caller, or a concurrent request created it first |
| 409 | `setup_required` | the version has no language yet (`lang` `""`): the site default is not confirmed |
| 409 | `language_unavailable` | one neutral answer: the language is disabled on the site (the chosen site on `POST /v1/article/` with `site`), or its version exists but is not readable by the caller, or a concurrent request created it first |
| 409 | `setup_required` | the version has no language yet (`lang` `""`): the site default is not confirmed (the chosen site on `POST /v1/article/` with `site`) |
| 409 | `slug_taken` | a `PATCH` slug used by another article of the same site and language (`X-Slug-Suggestion`) |
| 409 | `slug_conflict` | a move would put an address that already exists at the destination (`X-Conflicting-Articles`) |
| 409 | `story_move_required` | `PATCH site_id` on a version whose story holds several versions |
| 409 | `story_busy` | the database could not lock the rows in time (lock timeout, deadlock or serialization failure); retry |
| 422 | `invalid_language` | the tag is not a language tag |
| 422 | `unsupported_language` | a valid tag outside the supported list |
| 422 | `language_not_enabled` | the site has no language row for the tag (creation), or a destination site lacks or has disabled a language of the story (move, `X-Missing-Languages`) |
| 422 | `language_not_enabled` | the site (the chosen one on `POST /v1/article/` with `site`) has no language row for the tag (creation), or a destination site lacks or has disabled a language of the story (move, `X-Missing-Languages`) |
| 422 | `language_disabled` | publishing an offline version whose language is disabled on its site |
| 422 | `source_other_story` | the source belongs to another story |
@@ -138,7 +138,18 @@ Answers `201` with the created article object. Order of checks: permission, path
### `POST /v1/article/` without `story_id`
Unchanged: it creates a new story with one offline version. `lang` is now optional: absent, the version gets the starting language of the site as before; present, it is normalized and must be an enabled language of the default site (`422 invalid_language`, `unsupported_language`, `language_not_enabled`; `409 language_unavailable` when disabled; `409 setup_required` before the site default is confirmed).
It creates a new story with one offline version, on the default site unless the body names a site.
```json
{"title": "Neuf", "lang": "fr", "site": "atelier"}
```
- `site` is optional: the site key. Absent or `null`, the default site is used exactly as before. A present `site` is validated like any site key (`400` with the plain-text body `invalid_site_key` for `""` or `Bad Key!`), then looked up (`404 site_not_found` for an unknown key, a plain text body, nothing stored). Every caller who may create articles may create on every site: `content.create` is not scoped to a site.
- `lang` is optional. Absent, the version gets the starting language of the chosen site; present, it is normalized and must be an enabled language of the chosen site (`422 invalid_language`, `unsupported_language`, `language_not_enabled`; `409 language_unavailable` when disabled on that site; `409 setup_required` before that site's default language is confirmed). The same tag can therefore be accepted on one site and refused on another.
- The slug is generated from the title and made free within the chosen site and language only.
- `site` with `story_id`, `source_article_id` or `copy` answers `400` with the body mismatch message and stores nothing. `POST /v1/article/{id}/translations` has no `site` field: the translation keeps the site of its source and an unknown `site` key in its body is ignored.
- The response is the created article object (`201`) with the same keys as before; `site_id` is the id of the chosen site. The article is offline, so it is invisible to anonymous reads and public lists, and `GET /v1/article/?site=<key>` lists it for readers.
- `article.create` records `params.site` (the site key) on success; a refusal records one failed event with a plain reason (refusals by validation, `401` and `403` happen before the use case and record nothing).
## Deleting