drupal-mcp-connector 2.14.1 → 2.15.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-create-translation.md +9 -6
- package/.agents/commands/drupal-entity-update.md +3 -2
- package/.agents/commands/drupal-get-node.md +4 -3
- package/.agents/commands/drupal-get-paragraph.md +5 -3
- package/.agents/commands/drupal-get-revision.md +4 -3
- package/.agents/commands/drupal-list-translations.md +3 -3
- package/.agents/commands/drupal-update-node.md +6 -5
- package/.agents/commands/drupal-update-paragraph.md +6 -3
- package/CHANGELOG.md +73 -0
- package/README.md +5 -3
- package/package.json +1 -1
- package/scripts/generate-commands.js +63 -4
- package/scripts/install-commands.js +33 -10
- package/src/lib/backends/backend-interface.js +5 -3
- package/src/lib/backends/jsonapi.js +79 -9
- package/src/lib/draft-write.js +391 -18
- package/src/lib/entity-response.js +25 -0
- package/src/lib/patch-preflight.js +133 -8
- package/src/lib/path-alias.js +37 -0
- package/src/tools/entities.js +5 -2
- package/src/tools/nodes.js +128 -25
- package/src/tools/paragraphs.js +51 -11
- package/src/tools/revisions.js +35 -3
- package/src/tools/translations.js +156 -82
package/src/tools/revisions.js
CHANGED
|
@@ -21,8 +21,10 @@
|
|
|
21
21
|
import { getSiteConfig } from "../lib/config.js";
|
|
22
22
|
import { resolveBackend } from "../lib/backends/index.js";
|
|
23
23
|
import {
|
|
24
|
-
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed,
|
|
24
|
+
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed, redactCanonicalEntity,
|
|
25
25
|
} from "../lib/security.js";
|
|
26
|
+
import { assertDraftLangcode, readDraftTranslation, readTranslationInventory } from "../lib/draft-write.js";
|
|
27
|
+
import { omitLiveComputedMetatag } from "../lib/entity-response.js";
|
|
26
28
|
|
|
27
29
|
// Attributes that describe the entity's identity / revision bookkeeping / paths.
|
|
28
30
|
// These are read-only or server-managed and must NOT be replayed on a revert.
|
|
@@ -205,12 +207,41 @@ async function listRevisions({ site: siteName, type, id }) {
|
|
|
205
207
|
* @returns {Promise<?object>} The revision descriptor, or null if not found.
|
|
206
208
|
* @throws {SecurityError} If reading nodes/bundle is not permitted.
|
|
207
209
|
*/
|
|
208
|
-
async function getRevision({ site: siteName, type, id, version }) {
|
|
210
|
+
async function getRevision({ site: siteName, type, id, version, langcode }) {
|
|
209
211
|
const site = getSiteConfig(siteName);
|
|
210
212
|
const sec = resolveSecurityConfig(site);
|
|
211
213
|
assertReadAllowed(sec, "node", type);
|
|
212
214
|
const backend = await resolveBackend(site);
|
|
213
215
|
|
|
216
|
+
if (langcode) {
|
|
217
|
+
const targetLang = assertDraftLangcode(langcode);
|
|
218
|
+
const inventory = await readTranslationInventory(backend, { entityType: "node", bundle: type, id });
|
|
219
|
+
const workingRow = (inventory.working?.translations ?? []).find((row) => row.langcode === targetLang);
|
|
220
|
+
if (!inventory.live?.vid || !inventory.working?.vid || !workingRow || workingRow.status !== false) {
|
|
221
|
+
throw new Error("No unpublished working translation is addressable for this revision.");
|
|
222
|
+
}
|
|
223
|
+
const entity = await readDraftTranslation(backend, {
|
|
224
|
+
entityType: "node", bundle: type, id, langcode: targetLang,
|
|
225
|
+
draftRevision: { liveVid: inventory.live.vid, workingVid: inventory.working.vid },
|
|
226
|
+
});
|
|
227
|
+
const redacted = omitLiveComputedMetatag(redactCanonicalEntity(entity, sec, "node"));
|
|
228
|
+
return {
|
|
229
|
+
entityType: "node",
|
|
230
|
+
bundle: type,
|
|
231
|
+
id,
|
|
232
|
+
vid: inventory.working.vid,
|
|
233
|
+
langcode: targetLang,
|
|
234
|
+
status: redacted.status,
|
|
235
|
+
attributes: {
|
|
236
|
+
title: redacted.title,
|
|
237
|
+
langcode: redacted.langcode,
|
|
238
|
+
status: redacted.status,
|
|
239
|
+
...redacted.fields,
|
|
240
|
+
},
|
|
241
|
+
relationships: redacted.relationships ?? {},
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
|
|
214
245
|
const resource = await fetchRevisionResource(backend, "node", type, id, version);
|
|
215
246
|
if (!resource) return null;
|
|
216
247
|
|
|
@@ -314,13 +345,14 @@ export const definitions = [
|
|
|
314
345
|
{
|
|
315
346
|
name: "drupal_get_revision",
|
|
316
347
|
description:
|
|
317
|
-
"Fetch a single revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Read-only; attributes are redacted per security policy.",
|
|
348
|
+
"Fetch a single revision of a content node by version id or alias. `version` may be a numeric vid (e.g. 42), an explicit 'id:<vid>', or the relative aliases 'rel:latest-version' / 'rel:working-copy'. Pass langcode to read the unpublished working translation of that node (Sentinel). Read-only; attributes are redacted per security policy.",
|
|
318
349
|
inputSchema: {
|
|
319
350
|
type: "object", required: ["type", "id", "version"],
|
|
320
351
|
properties: {
|
|
321
352
|
site: { type: "string" },
|
|
322
353
|
type: { type: "string", description: "Content type machine name" },
|
|
323
354
|
id: { type: "string", description: "Node UUID" },
|
|
355
|
+
langcode: { type: "string", description: "Target language for the unpublished working translation (e.g. 'es')." },
|
|
324
356
|
version: {
|
|
325
357
|
type: ["string", "number"],
|
|
326
358
|
description: "Numeric vid, 'id:<vid>', 'rel:latest-version', or 'rel:working-copy'.",
|
|
@@ -1,57 +1,55 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Tool group: Content translations (multilingual / content_translation).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* surface multilingual handling cleanly we expose:
|
|
4
|
+
* Core JSON:API does not add a translation by PATCHing `langcode` on the
|
|
5
|
+
* canonical entity — that mutates the default language. Governed node
|
|
6
|
+
* translations go through Sentinel's draft-translation surface:
|
|
8
7
|
*
|
|
9
|
-
* - drupal_list_translations —
|
|
10
|
-
* - drupal_create_translation —
|
|
11
|
-
* PATCHing the entity with the target `langcode` plus the supplied fields.
|
|
8
|
+
* - drupal_list_translations — inventory of live and working languages
|
|
9
|
+
* - drupal_create_translation — add a language as an unpublished forward draft
|
|
12
10
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* are rejected by Drupal and listing only ever reports the single language.
|
|
11
|
+
* Continuation of an existing unpublished translation is drupal_update_node
|
|
12
|
+
* with `langcode`. Reads of that draft are drupal_get_node / drupal_get_revision
|
|
13
|
+
* with `langcode`.
|
|
17
14
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* resource. Enumerating ALL translations of an entity requires either the
|
|
22
|
-
* contrib "JSON:API Translation" module or the Drush bridge, neither of which
|
|
23
|
-
* is assumed here — see the `note` field on the list result.
|
|
24
|
-
*
|
|
25
|
-
* Reads are redacted per the site security policy; the create path is a
|
|
26
|
-
* governed write (asserts the "update" operation — a translation is a facet of
|
|
27
|
-
* an existing entity, not a new entity).
|
|
15
|
+
* Paragraph field-value translation uses the same surface with
|
|
16
|
+
* entityType "paragraph" and the pinned paragraph revision. Image alt is a
|
|
17
|
+
* node relationship (`meta.alt`) with the shared file target unchanged.
|
|
28
18
|
*/
|
|
29
19
|
|
|
30
20
|
import { getSiteConfig } from "../lib/config.js";
|
|
31
21
|
import { resolveBackend } from "../lib/backends/index.js";
|
|
32
22
|
import {
|
|
33
|
-
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed,
|
|
23
|
+
resolveSecurityConfig, assertReadAllowed, assertWriteAllowed, assertPublishAllowed,
|
|
24
|
+
redactCanonicalEntity,
|
|
34
25
|
} from "../lib/security.js";
|
|
35
26
|
import { validateUuid, validateMachineName } from "../lib/validate.js";
|
|
27
|
+
import { applySafeDraftDefault } from "../lib/moderation-default.js";
|
|
28
|
+
import { omitLiveComputedMetatag } from "../lib/entity-response.js";
|
|
29
|
+
import { entityRevisionId } from "../lib/write-revision.js";
|
|
30
|
+
import { paragraphRevisionId } from "../lib/err-relationships.js";
|
|
31
|
+
import {
|
|
32
|
+
assertDraftLangcode,
|
|
33
|
+
createTranslationDraft,
|
|
34
|
+
readTranslationInventory,
|
|
35
|
+
resolveNodeTranslationPair,
|
|
36
|
+
} from "../lib/draft-write.js";
|
|
36
37
|
|
|
37
38
|
const LIST_NOTE =
|
|
38
|
-
"
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
39
|
+
"Live languages are those on the default revision. Working languages are the " +
|
|
40
|
+
"unpublished forward revision when the principal can view it. Core JSON:API " +
|
|
41
|
+
"alone cannot enumerate translations; this inventory requires Sentinel's " +
|
|
42
|
+
"mcp-translations endpoint.";
|
|
43
|
+
|
|
44
|
+
const FALLBACK_NOTE =
|
|
45
|
+
"Sentinel's translation inventory is unavailable. Core JSON:API served one " +
|
|
46
|
+
"language for this resource; it does not prove other translations are absent.";
|
|
43
47
|
|
|
44
48
|
/**
|
|
45
|
-
* List
|
|
46
|
-
*
|
|
47
|
-
* Reads the entity through the backend's validated rawQuery (so path segments
|
|
48
|
-
* are checked and content_moderation handling is reused) and reports its
|
|
49
|
-
* `langcode`. The entity type defaults to "node"; pass `entityType` for others.
|
|
49
|
+
* List live and working translation langcodes for an entity.
|
|
50
50
|
*
|
|
51
51
|
* @param {object} args - { site?, entityType?, type, id }.
|
|
52
|
-
* `type` is the bundle machine name; `entityType` defaults to "node".
|
|
53
52
|
* @returns {Promise<object|null>} A translation summary, or null if not found.
|
|
54
|
-
* @throws {SecurityError} If reading the type/bundle is not permitted.
|
|
55
53
|
*/
|
|
56
54
|
async function listTranslations({ site: siteName, entityType = "node", type, id }) {
|
|
57
55
|
validateMachineName(entityType, "entityType");
|
|
@@ -63,6 +61,36 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
63
61
|
assertReadAllowed(sec, entityType, type);
|
|
64
62
|
|
|
65
63
|
const backend = await resolveBackend(site);
|
|
64
|
+
if ((entityType === "node" || entityType === "paragraph") && typeof backend.rawQuery === "function") {
|
|
65
|
+
try {
|
|
66
|
+
const meta = await readTranslationInventory(backend, { entityType, bundle: type, id });
|
|
67
|
+
const liveLangs = (meta.live?.translations ?? []).map((row) => row.langcode);
|
|
68
|
+
const workingLangs = (meta.working?.translations ?? []).map((row) => row.langcode);
|
|
69
|
+
const langcodes = [...new Set([...liveLangs, ...workingLangs])];
|
|
70
|
+
return {
|
|
71
|
+
id,
|
|
72
|
+
entityType,
|
|
73
|
+
bundle: type,
|
|
74
|
+
defaultLangcode: meta.defaultLangcode ?? liveLangs[0] ?? null,
|
|
75
|
+
langcodes,
|
|
76
|
+
live: meta.live ?? null,
|
|
77
|
+
working: meta.working ?? null,
|
|
78
|
+
translations: (meta.working?.translations ?? meta.live?.translations ?? []).map((row) => ({
|
|
79
|
+
langcode: row.langcode,
|
|
80
|
+
default: Boolean(row.default),
|
|
81
|
+
status: row.status,
|
|
82
|
+
title: row.title,
|
|
83
|
+
moderation_state: row.moderation_state,
|
|
84
|
+
})),
|
|
85
|
+
note: LIST_NOTE,
|
|
86
|
+
};
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!/does not provide Sentinel's governed draft-translation endpoint/.test(String(error?.message))) {
|
|
89
|
+
throw error;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
66
94
|
const res = await backend.rawQuery({ path: `/jsonapi/${entityType}/${type}/${id}` });
|
|
67
95
|
const data = res?.data;
|
|
68
96
|
if (!data) return null;
|
|
@@ -78,69 +106,108 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
78
106
|
defaultLangcode,
|
|
79
107
|
langcodes,
|
|
80
108
|
translations,
|
|
81
|
-
note:
|
|
109
|
+
note: FALLBACK_NOTE,
|
|
82
110
|
};
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
/**
|
|
86
|
-
* Create
|
|
87
|
-
*
|
|
88
|
-
* Governed write. Implemented as a PATCH (via the backend's updateEntity) that
|
|
89
|
-
* sets the target `langcode` alongside the supplied attributes. With
|
|
90
|
-
* content_translation enabled and the bundle marked translatable, Drupal stores
|
|
91
|
-
* the supplied fields against the requested language. If the bundle is not
|
|
92
|
-
* translatable, Drupal rejects the write and the error surfaces to the caller.
|
|
114
|
+
* Create a translation as an unpublished non-default draft revision.
|
|
93
115
|
*
|
|
94
|
-
*
|
|
116
|
+
* Does not PATCH canonical langcode. Requires Sentinel's translation endpoint.
|
|
117
|
+
* An existing translation is a conflict. English live fields stay unchanged.
|
|
95
118
|
*
|
|
96
119
|
* @param {object} args - { site?, entityType?, type, id, langcode, attributes? }.
|
|
97
|
-
*
|
|
98
|
-
* target language (e.g. "de"); `attributes` are the translated field values.
|
|
99
|
-
* @returns {Promise<object>} The updated/redacted entity descriptor.
|
|
100
|
-
* @throws {SecurityError} If writing the type/bundle is not permitted.
|
|
101
|
-
* @throws {Error} If langcode/id/type are invalid, or Drupal rejects the write.
|
|
120
|
+
* @returns {Promise<object>} The created translation, redacted.
|
|
102
121
|
*/
|
|
103
|
-
async function createTranslation({
|
|
122
|
+
async function createTranslation({
|
|
123
|
+
site: siteName, entityType = "node", type, id, langcode, attributes = {},
|
|
124
|
+
relationships, revisionId, dryRun = false,
|
|
125
|
+
}) {
|
|
104
126
|
validateMachineName(entityType, "entityType");
|
|
105
127
|
validateMachineName(type, "type");
|
|
106
128
|
validateUuid(id);
|
|
107
|
-
|
|
108
|
-
// variant; validate it as a machine-name-like token (e.g. "en", "pt_br",
|
|
109
|
-
// "zh_hans") to block injection / malformed values.
|
|
110
|
-
validateMachineName(langcode, "langcode");
|
|
129
|
+
const targetLang = assertDraftLangcode(langcode);
|
|
111
130
|
|
|
112
131
|
const site = getSiteConfig(siteName);
|
|
113
132
|
const sec = resolveSecurityConfig(site);
|
|
114
|
-
// A translation is a facet of an existing entity → "update", not "create".
|
|
115
133
|
assertWriteAllowed(sec, "update", entityType, type);
|
|
116
134
|
|
|
135
|
+
if (entityType !== "node" && entityType !== "paragraph") {
|
|
136
|
+
throw new Error("Governed translation create is implemented for nodes and paragraphs.");
|
|
137
|
+
}
|
|
138
|
+
|
|
117
139
|
const backend = await resolveBackend(site);
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
140
|
+
const existing = await backend.getEntity({ entityType, bundle: type, id });
|
|
141
|
+
if (!existing) {
|
|
142
|
+
throw new Error("The entity was not found.");
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
let draftRevision;
|
|
146
|
+
let drafted = { ...attributes };
|
|
147
|
+
delete drafted.langcode;
|
|
148
|
+
if (entityType === "paragraph") {
|
|
149
|
+
const pinned = revisionId ?? paragraphRevisionId(existing);
|
|
150
|
+
if (pinned === null || pinned === undefined || pinned === "") {
|
|
151
|
+
throw new Error("Paragraph translation create requires a paragraph revision ID (the host pin).");
|
|
152
|
+
}
|
|
153
|
+
draftRevision = { revisionId: pinned };
|
|
154
|
+
assertPublishAllowed(sec, drafted);
|
|
155
|
+
} else {
|
|
156
|
+
draftRevision = await resolveNodeTranslationPair(backend, {
|
|
157
|
+
entityType, bundle: type, id, existing,
|
|
158
|
+
});
|
|
159
|
+
if (drafted.status === undefined && drafted.moderation_state === undefined) {
|
|
160
|
+
drafted.moderation_state = "draft";
|
|
161
|
+
}
|
|
162
|
+
drafted = await applySafeDraftDefault({
|
|
163
|
+
backend, entityType, bundle: type, id, attributes: drafted, existingEntity: existing,
|
|
164
|
+
});
|
|
165
|
+
assertPublishAllowed(sec, drafted);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (dryRun) {
|
|
169
|
+
await createTranslationDraft(backend, {
|
|
170
|
+
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
171
|
+
}, true);
|
|
172
|
+
return {
|
|
173
|
+
dryRun: true, operation: "create_translation", entityType, bundle: type, id,
|
|
174
|
+
langcode: targetLang, attributes: drafted, ...(relationships ? { relationships } : {}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const created = await createTranslationDraft(backend, {
|
|
179
|
+
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
123
180
|
});
|
|
124
|
-
|
|
181
|
+
const redacted = omitLiveComputedMetatag(redactCanonicalEntity(created, sec, entityType));
|
|
182
|
+
if (entityType !== "node") return redacted;
|
|
183
|
+
const workingVid = entityRevisionId(created) ?? draftRevision.workingVid;
|
|
184
|
+
const liveVid = draftRevision.liveVid;
|
|
185
|
+
if (liveVid === undefined || liveVid === null) {
|
|
186
|
+
if (workingVid === undefined || workingVid === null) return redacted;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
...redacted,
|
|
190
|
+
_revisions: {
|
|
191
|
+
...(liveVid !== undefined && liveVid !== null ? { live: liveVid } : {}),
|
|
192
|
+
...(workingVid !== undefined && workingVid !== null ? { working: workingVid } : {}),
|
|
193
|
+
},
|
|
194
|
+
};
|
|
125
195
|
}
|
|
126
196
|
|
|
127
|
-
// ---------------------------------------------------------------------------
|
|
128
|
-
// Tool definitions
|
|
129
|
-
// ---------------------------------------------------------------------------
|
|
130
|
-
|
|
131
197
|
export const definitions = [
|
|
132
198
|
{
|
|
133
199
|
name: "drupal_list_translations",
|
|
134
200
|
description:
|
|
135
|
-
"List
|
|
136
|
-
"
|
|
137
|
-
"
|
|
201
|
+
"List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's " +
|
|
202
|
+
"translation inventory when available (live default revision vs unpublished working " +
|
|
203
|
+
"draft). Core JSON:API alone serves one language and cannot prove others are absent. " +
|
|
204
|
+
"Defaults to node.",
|
|
138
205
|
inputSchema: {
|
|
139
206
|
type: "object", required: ["type", "id"],
|
|
140
207
|
properties: {
|
|
141
208
|
site: { type: "string", description: "Named site (omit for default)" },
|
|
142
209
|
entityType: { type: "string", description: "Entity type machine name. Default: 'node'." },
|
|
143
|
-
type: { type: "string", description: "Bundle machine name, e.g. '
|
|
210
|
+
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page'" },
|
|
144
211
|
id: { type: "string", description: "Entity UUID" },
|
|
145
212
|
},
|
|
146
213
|
},
|
|
@@ -148,28 +215,35 @@ export const definitions = [
|
|
|
148
215
|
{
|
|
149
216
|
name: "drupal_create_translation",
|
|
150
217
|
description:
|
|
151
|
-
"Create
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
218
|
+
"Create a translation as an unpublished non-default draft (governed write). " +
|
|
219
|
+
"Adds the target language beside the default language; it does not PATCH langcode on " +
|
|
220
|
+
"the canonical entity. When an English working draft already exists, both live and " +
|
|
221
|
+
"working revision IDs are sent (If-Match) so Sentinel will add the language on that " +
|
|
222
|
+
"draft (#282). English live title, body, status, alias, default revision, and " +
|
|
223
|
+
"paragraph ERR pins stay unchanged. An existing translation is a conflict, not an overwrite. " +
|
|
224
|
+
"The response includes `_revisions.live` / `_revisions.working` when known. " +
|
|
225
|
+
"Computed `metatag` is omitted on the draft body because JSON:API resolves it from the live default (#283); use field_metatags. " +
|
|
226
|
+
"Continue a node draft with drupal_update_node and langcode; continue a paragraph with " +
|
|
227
|
+
"drupal_update_paragraph and langcode. Image alt is a relationship (same file UUID, " +
|
|
228
|
+
"meta.alt). For paragraphs pass revisionId as the host pin. Requires Sentinel's " +
|
|
229
|
+
"draft-translation endpoint. Publication stays denied for content-tier callers.",
|
|
155
230
|
inputSchema: {
|
|
156
231
|
type: "object", required: ["type", "id", "langcode"],
|
|
157
232
|
properties: {
|
|
158
|
-
site:
|
|
159
|
-
entityType:
|
|
160
|
-
type:
|
|
161
|
-
id:
|
|
162
|
-
langcode:
|
|
163
|
-
attributes:
|
|
233
|
+
site: { type: "string" },
|
|
234
|
+
entityType: { type: "string", description: "Entity type machine name. Default: 'node'. Use 'paragraph' for paragraph field values." },
|
|
235
|
+
type: { type: "string", description: "Bundle machine name, e.g. 'basic_page' or 'p_hero'" },
|
|
236
|
+
id: { type: "string", description: "Entity UUID" },
|
|
237
|
+
langcode: { type: "string", description: "Target language code, e.g. 'es', 'de', 'pt-br'" },
|
|
238
|
+
attributes: { type: "object", description: "Translated field values keyed by Drupal machine name" },
|
|
239
|
+
relationships: { type: "object", description: "JSON:API relationships. Use for image alt (same file UUID, meta.alt)." },
|
|
240
|
+
revisionId: { type: "string", description: "Paragraph revision id the host already pins. Required for Home-shaped non-default pins." },
|
|
241
|
+
dryRun: { type: "boolean", description: "Validate without saving" },
|
|
164
242
|
},
|
|
165
243
|
},
|
|
166
244
|
},
|
|
167
245
|
];
|
|
168
246
|
|
|
169
|
-
// ---------------------------------------------------------------------------
|
|
170
|
-
// Handler map
|
|
171
|
-
// ---------------------------------------------------------------------------
|
|
172
|
-
|
|
173
247
|
export const handlers = {
|
|
174
248
|
drupal_list_translations: listTranslations,
|
|
175
249
|
drupal_create_translation: createTranslation,
|