drupal-mcp-connector 3.1.0 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/commands/drupal-draft-nested-components.md +26 -0
- package/.agents/commands/drupal-entity-update.md +3 -3
- package/.agents/commands/drupal-update-node.md +1 -1
- package/CHANGELOG.md +61 -0
- package/package.json +7 -7
- package/src/lib/backends/jsonapi.js +18 -5
- package/src/lib/canonical-draft.js +795 -0
- package/src/lib/component-draft.js +16 -0
- package/src/lib/nested-draft.js +1404 -0
- package/src/lib/operations.js +1 -1
- package/src/lib/patch-preflight.js +34 -0
- package/src/lib/write-revision.js +66 -16
- package/src/tools/bulk.js +5 -9
- package/src/tools/entities.js +5 -11
- package/src/tools/index.js +2 -1
- package/src/tools/nested.js +68 -0
- package/src/tools/nodes.js +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: "Replace nested paragraphs on a node by creating new unpublished paragraphs and pinning a new direct parent from the node's unpublished draft. children is the full new list: { op: 'keep', id } copies an existing child pin, { op: 'replace', id, type, attributes, translations? } or { op: 'insert', type, attributes, translations? } creates a new child. Omitting a child leaves it off the draft only; it is not deleted. The published host and every paragraph it already pins stay unchanged. Never edits a live child and never sends nested UUIDs as mcp_components (use drupal_update_node components for a direct paragraph's attributes). Reusable library items and from_library children are refused; update the library item with drupal_entity_update. The first host draft requires MCP Sentinel 2.29.0 (open_draft). An existing working copy continues through /mcp-draft. When the host inventory advertises nested_replacement, one /mcp-draft carries meta.mcp_nested_replacement (parent is the parentId input), the connector creates no paragraphs, and it does not also send the host paragraph field. dryRun then uses that non-saving preflight (serverPreflight sentinel_draft). Hosts that do not advertise nested_replacement keep this sequence, including resumeParentId, and dryRun leaves serverPreflight none. New paragraphs are created in the host's default language. The new parent keeps the published parent's other references and replaces only the child field. Non-default translations on the component must be supplied on each replace or insert; they are created on the new paragraphs, and the component's own translations are copied onto the new parent. dryRun checks paragraph-create permission, creates nothing, and does not evaluate the host payload. If paragraph creates succeed and the host pin fails, the error lists the prepared UUIDs. When a parent was prepared, retry with resumeParentId and the same children list; that retry checks the prepared paragraph's fields, other references, and child revision pins before any host write. A failure before a parent exists says to delete the prepared paragraphs and does not offer a host retry. A lost response is uncertain until a re-read proves the prepared revision is pinned; the same UUID at another revision is not success. A second call after success is refused while the forward draft no longer pins the published component. Governed by the site security policy. Nothing is published."
|
|
3
|
+
argument-hint: "<bundle> <id> <field> <parentId> <childField> <children> [site] [liveRevisionId] [resumeParentId] [dryRun]"
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
Call the MCP tool `drupal_draft_nested_components`.
|
|
7
|
+
|
|
8
|
+
Replace nested paragraphs on a node by creating new unpublished paragraphs and pinning a new direct parent from the node's unpublished draft. children is the full new list: { op: 'keep', id } copies an existing child pin, { op: 'replace', id, type, attributes, translations? } or { op: 'insert', type, attributes, translations? } creates a new child. Omitting a child leaves it off the draft only; it is not deleted. The published host and every paragraph it already pins stay unchanged. Never edits a live child and never sends nested UUIDs as mcp_components (use drupal_update_node components for a direct paragraph's attributes). Reusable library items and from_library children are refused; update the library item with drupal_entity_update. The first host draft requires MCP Sentinel 2.29.0 (open_draft). An existing working copy continues through /mcp-draft. When the host inventory advertises nested_replacement, one /mcp-draft carries meta.mcp_nested_replacement (parent is the parentId input), the connector creates no paragraphs, and it does not also send the host paragraph field. dryRun then uses that non-saving preflight (serverPreflight sentinel_draft). Hosts that do not advertise nested_replacement keep this sequence, including resumeParentId, and dryRun leaves serverPreflight none. New paragraphs are created in the host's default language. The new parent keeps the published parent's other references and replaces only the child field. Non-default translations on the component must be supplied on each replace or insert; they are created on the new paragraphs, and the component's own translations are copied onto the new parent. dryRun checks paragraph-create permission, creates nothing, and does not evaluate the host payload. If paragraph creates succeed and the host pin fails, the error lists the prepared UUIDs. When a parent was prepared, retry with resumeParentId and the same children list; that retry checks the prepared paragraph's fields, other references, and child revision pins before any host write. A failure before a parent exists says to delete the prepared paragraphs and does not offer a host retry. A lost response is uncertain until a re-read proves the prepared revision is pinned; the same UUID at another revision is not success. A second call after success is refused while the forward draft no longer pins the published component. Governed by the site security policy. Nothing is published.
|
|
9
|
+
|
|
10
|
+
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
|
+
|
|
12
|
+
**Required:**
|
|
13
|
+
- `bundle` (string): Node bundle machine name, e.g. 'page'
|
|
14
|
+
- `id` (string): Node UUID
|
|
15
|
+
- `field` (string): Host entity-reference-revisions field that pins the direct parent, e.g. 'field_components'
|
|
16
|
+
- `parentId` (string): UUID of the direct paragraph the published host pins. Not a nested child.
|
|
17
|
+
- `childField` (string): Entity-reference-revisions field on that direct paragraph, e.g. 'field_items'
|
|
18
|
+
- `children` (array (pass as JSON)): Full new child list, in order. keep: { op, id }. replace: { op, id, type, attributes, translations? }. insert: { op, type, attributes, translations? }. translations is [{ langcode, attributes }] for each non-default language the component already has. Relationships inside a child are refused.
|
|
19
|
+
|
|
20
|
+
**Optional:**
|
|
21
|
+
- `site` (string): Named site from connector config. Omit only on reads: multi-site configs fall back to defaultSite (often local/dev, not production). Writes require an explicit site when more than one site is configured. Every response includes `_target` { name, baseUrl, source } (`hint` when you passed site, `default` when you did not).
|
|
22
|
+
- `liveRevisionId` (string): Optional published revision id. Refused before any create when it is not the current live revision.
|
|
23
|
+
- `resumeParentId` (string): Prepared parent UUID from a previous attempt whose host pin did not finish. Retries only the host pin. Must not be a paragraph the published host already pins.
|
|
24
|
+
- `dryRun` (boolean (true/false)): When nested_replacement is advertised, run that non-saving /mcp-draft preflight and create nothing (checks.serverPreflight is sentinel_draft). Otherwise read the host and component, then return without creating paragraphs or evaluating the host draft payload (checks.serverPreflight is none).
|
|
25
|
+
|
|
26
|
+
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
|
-
description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran."
|
|
2
|
+
description: "Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran. When the draft inventory advertises open_draft, a published moderated paragraphs_library_item or block_content opens and continues through /mcp-draft with If-Match live or live:working, no mcp_components, and a re-read of the published pins. A continuation compares the forward revision with the pre-write working pins. When open_draft is not advertised, a published moderated paragraphs_library_item or block_content with no forward revision is saved by that canonical draft PATCH, then the published revision, rel:working-copy, and paragraph pins (including order) are re-read. The result fails if the published revision changed or no forward revision appeared. An existing forward draft, a missing revision id, or a denied version read is refused before any write. An unmoderated item keeps the ordinary update. A completed 4xx is returned when that re-read matches the pre-write revision and pins; a lost response stays uncertain (#420)."
|
|
3
3
|
argument-hint: "<entityType> <bundle> <id> [site] [langcode] [attributes] [relationships] [dryRun] [returning]"
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
Call the MCP tool `drupal_entity_update`.
|
|
7
7
|
|
|
8
|
-
Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran.
|
|
8
|
+
Update an existing entity of any Drupal entity type. Only include attributes/relationships you want to change. Published moderated targets without an explicit attributes.moderation_state default to moderation_state 'draft' (forward revision). Paragraph / ERR identifiers are resolved to include meta.target_revision_id before PATCH; the write fails if any ref cannot be resolved. On moderated targets a non-saving PATCH preflight runs first (including dryRun) against the same URL the write will hit. An addressable node draft uses Sentinel's governed draft endpoint with live/working revision preconditions (#166); a stray revision with no addressable working copy still fails with revision-surgery language (#201). Preflight does not un-orphan paragraphs already created — probe the host before creating dependents. A dryRun that returns without a refusal is not proof the write will succeed: field access and entity validation are checked only when Sentinel's draft endpoint ran, and the result's `checks` block says which checks ran. When the draft inventory advertises open_draft, a published moderated paragraphs_library_item or block_content opens and continues through /mcp-draft with If-Match live or live:working, no mcp_components, and a re-read of the published pins. A continuation compares the forward revision with the pre-write working pins. When open_draft is not advertised, a published moderated paragraphs_library_item or block_content with no forward revision is saved by that canonical draft PATCH, then the published revision, rel:working-copy, and paragraph pins (including order) are re-read. The result fails if the published revision changed or no forward revision appeared. An existing forward draft, a missing revision id, or a denied version read is refused before any write. An unmoderated item keeps the ordinary update. A completed 4xx is returned when that re-read matches the pre-write revision and pins; a lost response stays uncertain (#420).
|
|
9
9
|
|
|
10
10
|
Parse the arguments supplied with this command into this tool's parameters:
|
|
11
11
|
|
|
@@ -19,7 +19,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
19
19
|
- `langcode` (string): Target language for an unpublished working translation (nodes). Continues that translation via Sentinel.
|
|
20
20
|
- `attributes` (object (pass as JSON))
|
|
21
21
|
- `relationships` (object (pass as JSON))
|
|
22
|
-
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, run the server-side preflight when one applies, and return a preview without the real write. The result's `checks` block says what was checked; `caveat` names what was not.
|
|
22
|
+
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, run the server-side preflight when one applies, and return a preview without the real write. The result's `checks` block says what was checked; `caveat` names what was not. An existing node draft (or a langcode translation draft), and a paragraphs_library_item or block_content whose inventory advertises open_draft, are checked with the real payload: Sentinel's non-saving draft endpoint applies the submitted fields through field access and validates the entity (`serverPreflight: sentinel_draft`). On other moderated targets an id-mismatch core PATCH with no fields checks entity update access and core's working-copy guard only; field access and entity validation are NOT checked (`core_patch_guard`), so the real write can still fail with a field-access 403 or a 422. Unmoderated targets get no server-side check at all (`none`). A changed/revision_timestamp gap on the default revision (possiblyPatchBlocked) is a warning on list_revisions, not a local dryRun refusal — the write proceeds to the core probe / Sentinel (#405). An actual Sentinel stale-copy refusal is still rewritten. Any refusal fails the dryRun.
|
|
23
23
|
- `returning` (one of: full, minimal): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
|
|
24
24
|
|
|
25
25
|
If a required parameter is missing, ask before calling — do not invent values. Coerce each value to its JSON type (booleans → true/false, numbers → numeric, object/array → parse JSON), then make the single tool call and summarize the result.
|
|
@@ -24,7 +24,7 @@ Parse the arguments supplied with this command into this tool's parameters:
|
|
|
24
24
|
- `langcode` (string): Target language for an unpublished working translation (e.g. 'es'). Continues that translation via Sentinel; does not create a missing translation and does not PATCH canonical langcode.
|
|
25
25
|
- `fields` (object (pass as JSON)): Scalar/attribute field values keyed by machine name. Formatted text: a string or { value, format?, summary? }. format must be in the field's allowed_formats; a single allowed format is used when omitted. Entity-reference fields go in `relationships`, not here.
|
|
26
26
|
- `relationships` (object (pass as JSON)): Entity-reference fields as JSON:API relationships, keyed by field machine name. Single-value uses { data: { type, id } }; multi-value uses { data: [{ type, id }, …] }. Paragraph / ERR items must carry meta.target_revision_id — the connector injects it when missing, and fails the write if it cannot. Image alt on a translation uses the existing file UUID plus meta.alt; replacing the file is refused.
|
|
27
|
-
- `components` (array (pass as JSON)): Field changes for paragraphs this node already references directly (hero, text blocks, CTA banners), saved in the same unpublished draft revision as the node fields. Each entry is { id: paragraph UUID, type: paragraph bundle such as 'p_hero', attributes: { field: value } }. Each changed paragraph gets a new revision that only the draft points at; the live page does not change until the draft is published. Opens the working copy from live when none exists. Default language only. Attribute values only: references (for example a media field) are refused, as are
|
|
27
|
+
- `components` (array (pass as JSON)): Field changes for paragraphs this node already references directly (hero, text blocks, CTA banners), saved in the same unpublished draft revision as the node fields. Each entry is { id: paragraph UUID, type: paragraph bundle such as 'p_hero', attributes: { field: value } }. Each changed paragraph gets a new revision that only the draft points at; the live page does not change until the draft is published. Opens the working copy from live when none exists. Default language only. Attribute values only: references (for example a media field) are refused, as are paragraphs the node does not reference. A nested paragraph is refused before any write; use drupal_draft_nested_components. Requires MCP Sentinel 2.29.0 or later; older hosts are refused before any write, with no direct paragraph write. A `path` change is refused on this path. The connector's paragraph write policy applies to each component. After the write each component is read back at its working revision and the submitted values compared; a value that did not land is an error. The result carries `_components` (each component's live and working pins and the fields verified), which survives `returning: 'minimal'`.
|
|
28
28
|
- `dryRun` (boolean (true/false)): Validate, resolve ERR identifiers, run the server-side preflight when one applies, and return a preview without the real write. The result's `checks` block says what was checked; `caveat` names what was not. Only an existing node draft (or a langcode translation draft) is checked with the real payload: Sentinel's non-saving draft endpoint applies the submitted fields through field access and validates the entity (`serverPreflight: sentinel_draft`). On other moderated targets an id-mismatch core PATCH with no fields checks entity update access and core's working-copy guard only; Drupal does not check field access or entity validation on that probe (`core_patch_guard`). Text format is resolved before the preview returns, including on that path: a known allowed_formats list is enforced, and an unknown list reuses the format stored on the field. Other fields can still fail the real write with a field-access 403 or a validation 422. Unmoderated targets get no server-side check at all (`none`). A changed/revision_timestamp gap on the default revision (possiblyPatchBlocked) is a warning on list_revisions, not a local dryRun refusal — the write proceeds to the core probe / Sentinel (#405). An actual Sentinel stale-copy refusal is still rewritten. Any refusal fails the dryRun.
|
|
29
29
|
- `returning` (one of: full, minimal): Response verbosity. "full" (default) returns the complete saved entity; "minimal" returns just identity + state (id, type, bundle, title, status, changed, url) — much smaller, recommended for bulk writes where the echoed body would dominate the response.
|
|
30
30
|
|
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [3.2.0] - 2026-10-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- **`drupal_draft_nested_components` (#421).** Replaces a nested paragraph
|
|
14
|
+
by creating new unpublished paragraphs and pinning a new direct parent
|
|
15
|
+
from the host's unpublished draft. The published host and the paragraphs
|
|
16
|
+
it already pins are not edited. `children` is the full new list (`keep`,
|
|
17
|
+
`replace`, `insert`; omit a child to leave it off the draft only). The
|
|
18
|
+
first host draft requires MCP Sentinel 2.29.0 (`open_draft`); an existing
|
|
19
|
+
working copy continues through `/mcp-draft`. There is no `mcp_components`
|
|
20
|
+
payload and no direct paragraph write. `dryRun` creates nothing and does
|
|
21
|
+
not evaluate the host payload. A host write that fails after preparation
|
|
22
|
+
reports the new paragraph UUIDs. When a parent was prepared, recovery is
|
|
23
|
+
`resumeParentId` with the same child list; that retry checks the prepared
|
|
24
|
+
paragraph's fields, other references, and child revision pins before any
|
|
25
|
+
host write. A failure before a parent exists says to delete the prepared
|
|
26
|
+
paragraphs. New paragraphs are created in the host's default language, and
|
|
27
|
+
the new parent keeps the published parent's other references. A lost
|
|
28
|
+
response stays uncertain until a re-read proves the prepared revision is
|
|
29
|
+
pinned. Reusable library items are refused; update those with
|
|
30
|
+
`drupal_entity_update`.
|
|
31
|
+
|
|
32
|
+
### Changed
|
|
33
|
+
- **Advertised library and custom-block drafts (#420, #422).** When the host
|
|
34
|
+
inventory lists `open_draft`, `drupal_entity_update` opens and continues a
|
|
35
|
+
published moderated `paragraphs_library_item` or `block_content` through
|
|
36
|
+
`/mcp-draft`. The body has no `mcp_components`. `If-Match` is the live
|
|
37
|
+
revision, or `live:working` when a forward draft already exists. The
|
|
38
|
+
published revision and its paragraph pins are re-read. A continuation
|
|
39
|
+
compares the forward revision with the pre-write working pins unless
|
|
40
|
+
relationships were submitted. When `open_draft` is not advertised, the
|
|
41
|
+
canonical draft PATCH remains, and an existing forward draft is still
|
|
42
|
+
refused before any write.
|
|
43
|
+
- **Advertised nested replacement (#421, #422).** When the host inventory
|
|
44
|
+
lists `nested_replacement`, `drupal_draft_nested_components` sends one
|
|
45
|
+
`/mcp-draft` with `meta.mcp_nested_replacement`. `parentId` is sent as
|
|
46
|
+
`parent`. The connector creates no paragraphs and does not also send the
|
|
47
|
+
host paragraph field. `dryRun` uses that non-saving preflight. Hosts that
|
|
48
|
+
do not advertise the operation keep the client sequence, including
|
|
49
|
+
`resumeParentId`.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
- **Reusable library and custom-block revision identity (#420).**
|
|
53
|
+
`drupal_internal__revision_id` is kept for `paragraphs_library_item` and
|
|
54
|
+
`block_content`. A published item with no forward revision is updated by a
|
|
55
|
+
canonical draft (`moderation_state: draft` or `review`); the connector then
|
|
56
|
+
re-reads the published revision, `rel:working-copy`, and the published
|
|
57
|
+
paragraph pins, including their order. Success requires a new working
|
|
58
|
+
revision and an unchanged published revision. `rel:latest-version` is the
|
|
59
|
+
latest default revision, so a difference there fails closed instead of
|
|
60
|
+
being treated as the draft. An existing forward draft is refused before
|
|
61
|
+
any write, because these types have no governed continuation endpoint. A
|
|
62
|
+
missing revision id, a denied version read, or a lost response fails
|
|
63
|
+
closed. A lost response is described from a follow-up re-read and is not
|
|
64
|
+
called a rollback. A completed 4xx is returned when that re-read matches
|
|
65
|
+
the published revision and pins; the same 4xx stays uncertain when the
|
|
66
|
+
re-read shows a change or cannot be finished. Drupal's working-copy "No
|
|
67
|
+
pending revision" response is treated as no draft only when its status is
|
|
68
|
+
403. An unmoderated library item or custom block keeps the ordinary update
|
|
69
|
+
and is not required to answer the version aliases.
|
|
70
|
+
|
|
10
71
|
## [3.1.0] - 2026-10-03
|
|
11
72
|
|
|
12
73
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "drupal-mcp-connector",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "Multi-site MCP server for Drupal over JSON:API and GraphQL, with governed writes and an SSH Drush bridge. init writes a stdio or Tailscale client config from a transport preset. doctor checks reachability, auth, and readiness, and reports version skew. A timestamp gap on a published revision no longer blocks the next draft.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -72,7 +72,7 @@
|
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
74
|
"@modelcontextprotocol/node": "^2.0.0",
|
|
75
|
-
"@modelcontextprotocol/server": "^2.
|
|
75
|
+
"@modelcontextprotocol/server": "^2.2.0",
|
|
76
76
|
"ajv": "^8.20.0",
|
|
77
77
|
"ajv-formats": "^3.0.1",
|
|
78
78
|
"graphql": "^17.0.0",
|
|
@@ -81,12 +81,12 @@
|
|
|
81
81
|
"ssh2": "^1.16.0"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
|
-
"@modelcontextprotocol/client": "^2.
|
|
84
|
+
"@modelcontextprotocol/client": "^2.2.0",
|
|
85
85
|
"eslint": "^10.4.1",
|
|
86
|
-
"eslint-plugin-n": "^18.
|
|
87
|
-
"eslint-plugin-security": "^4.
|
|
88
|
-
"globals": "^17.
|
|
89
|
-
"vitest": "^5.0.
|
|
86
|
+
"eslint-plugin-n": "^18.4.0",
|
|
87
|
+
"eslint-plugin-security": "^4.2.0",
|
|
88
|
+
"globals": "^17.13.0",
|
|
89
|
+
"vitest": "^5.0.3"
|
|
90
90
|
},
|
|
91
91
|
"overrides": {
|
|
92
92
|
"@hono/node-server": "^2.0.5",
|
|
@@ -349,12 +349,15 @@ export class JsonApiBackend extends Backend {
|
|
|
349
349
|
// Node reads expose the numeric nid advertised by schema discovery
|
|
350
350
|
// while the UUID remains the canonical top-level id (#237).
|
|
351
351
|
// Paragraph ERR attach needs the current revision id (#192).
|
|
352
|
+
// Library items and custom blocks use revision_id, not vid (#420).
|
|
352
353
|
// Node / revisionable writes need the working vs live vid (#166).
|
|
353
354
|
// Menu-link admin edit URLs need the numeric id (#398).
|
|
354
355
|
// Other drupal_internal__* attributes stay stripped.
|
|
355
356
|
return (entityType === "node" && k === "drupal_internal__nid")
|
|
356
357
|
|| k === "drupal_internal__vid"
|
|
357
|
-
|| (entityType === "paragraph"
|
|
358
|
+
|| ((entityType === "paragraph"
|
|
359
|
+
|| entityType === "paragraphs_library_item"
|
|
360
|
+
|| entityType === "block_content") && k === "drupal_internal__revision_id")
|
|
358
361
|
|| (entityType === "menu_link_content" && k === "drupal_internal__id");
|
|
359
362
|
}
|
|
360
363
|
return true;
|
|
@@ -594,18 +597,28 @@ export class JsonApiBackend extends Backend {
|
|
|
594
597
|
/**
|
|
595
598
|
* Create an entity via JSON:API POST. Retries without `status` on moderated
|
|
596
599
|
* bundles — see writeWithModerationFallback.
|
|
597
|
-
* @param {{entityType: string, bundle: string, attributes?: object, relationships?: object}} input
|
|
600
|
+
* @param {{entityType: string, bundle: string, attributes?: object, relationships?: object, langcode?: string}} input
|
|
601
|
+
* `langcode` creates the entity in that language (URL prefix, then language headers).
|
|
598
602
|
* @returns {Promise<import("../canonical.js").CanonicalEntity>} The created entity.
|
|
599
603
|
*/
|
|
600
|
-
async createEntity({ entityType, bundle, attributes = {}, relationships }) {
|
|
604
|
+
async createEntity({ entityType, bundle, attributes = {}, relationships, langcode }) {
|
|
601
605
|
const buildPayload = (attrs) => {
|
|
602
606
|
const payload = { data: { type: `${entityType}--${bundle}`, attributes: attrs } };
|
|
603
607
|
const rels = grantActorUid(entityType, relationships);
|
|
604
608
|
if (rels) payload.data.relationships = rels;
|
|
605
609
|
return payload;
|
|
606
610
|
};
|
|
607
|
-
const data = await this.writeWithModerationFallback(
|
|
608
|
-
|
|
611
|
+
const data = await this.writeWithModerationFallback(
|
|
612
|
+
this.resourcePath(entityType, bundle), "POST", buildPayload, attributes, langcode,
|
|
613
|
+
);
|
|
614
|
+
const entity = this.toCanonical(data.data);
|
|
615
|
+
try {
|
|
616
|
+
assertServedLanguage(entity, langcode);
|
|
617
|
+
} catch (err) {
|
|
618
|
+
if (err && typeof err === "object") err.entity = entity;
|
|
619
|
+
throw err;
|
|
620
|
+
}
|
|
621
|
+
return entity;
|
|
609
622
|
}
|
|
610
623
|
|
|
611
624
|
/**
|