drupal-mcp-connector 2.14.2 → 2.15.1
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 +7 -5
- package/.agents/commands/drupal-entity-update.md +1 -1
- package/.agents/commands/drupal-get-paragraph.md +5 -3
- package/.agents/commands/drupal-list-nodes.md +2 -2
- package/.agents/commands/drupal-list-translations.md +2 -2
- package/.agents/commands/drupal-update-node.md +2 -2
- package/.agents/commands/drupal-update-paragraph.md +6 -3
- package/CHANGELOG.md +72 -0
- package/README.md +6 -4
- package/package.json +2 -2
- 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 +160 -26
- package/src/lib/draft-write.js +220 -11
- package/src/lib/entity-response.js +25 -0
- package/src/lib/patch-preflight.js +122 -11
- package/src/lib/path-alias.js +37 -0
- package/src/tools/entities.js +1 -1
- package/src/tools/nodes.js +97 -24
- package/src/tools/paragraphs.js +120 -13
- package/src/tools/revisions.js +2 -1
- package/src/tools/translations.js +70 -41
|
@@ -12,8 +12,9 @@
|
|
|
12
12
|
* with `langcode`. Reads of that draft are drupal_get_node / drupal_get_revision
|
|
13
13
|
* with `langcode`.
|
|
14
14
|
*
|
|
15
|
-
* Paragraph field-value translation
|
|
16
|
-
*
|
|
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.
|
|
17
18
|
*/
|
|
18
19
|
|
|
19
20
|
import { getSiteConfig } from "../lib/config.js";
|
|
@@ -24,12 +25,14 @@ import {
|
|
|
24
25
|
} from "../lib/security.js";
|
|
25
26
|
import { validateUuid, validateMachineName } from "../lib/validate.js";
|
|
26
27
|
import { applySafeDraftDefault } from "../lib/moderation-default.js";
|
|
27
|
-
import {
|
|
28
|
+
import { omitLiveComputedMetatag } from "../lib/entity-response.js";
|
|
28
29
|
import { entityRevisionId } from "../lib/write-revision.js";
|
|
30
|
+
import { paragraphRevisionId } from "../lib/err-relationships.js";
|
|
29
31
|
import {
|
|
30
32
|
assertDraftLangcode,
|
|
31
33
|
createTranslationDraft,
|
|
32
34
|
readTranslationInventory,
|
|
35
|
+
resolveNodeTranslationPair,
|
|
33
36
|
} from "../lib/draft-write.js";
|
|
34
37
|
|
|
35
38
|
const LIST_NOTE =
|
|
@@ -58,7 +61,7 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
58
61
|
assertReadAllowed(sec, entityType, type);
|
|
59
62
|
|
|
60
63
|
const backend = await resolveBackend(site);
|
|
61
|
-
if (entityType === "node" && typeof backend.rawQuery === "function") {
|
|
64
|
+
if ((entityType === "node" || entityType === "paragraph") && typeof backend.rawQuery === "function") {
|
|
62
65
|
try {
|
|
63
66
|
const meta = await readTranslationInventory(backend, { entityType, bundle: type, id });
|
|
64
67
|
const liveLangs = (meta.live?.translations ?? []).map((row) => row.langcode);
|
|
@@ -117,7 +120,8 @@ async function listTranslations({ site: siteName, entityType = "node", type, id
|
|
|
117
120
|
* @returns {Promise<object>} The created translation, redacted.
|
|
118
121
|
*/
|
|
119
122
|
async function createTranslation({
|
|
120
|
-
site: siteName, entityType = "node", type, id, langcode, attributes = {},
|
|
123
|
+
site: siteName, entityType = "node", type, id, langcode, attributes = {},
|
|
124
|
+
relationships, revisionId, dryRun = false,
|
|
121
125
|
}) {
|
|
122
126
|
validateMachineName(entityType, "entityType");
|
|
123
127
|
validateMachineName(type, "type");
|
|
@@ -128,8 +132,8 @@ async function createTranslation({
|
|
|
128
132
|
const sec = resolveSecurityConfig(site);
|
|
129
133
|
assertWriteAllowed(sec, "update", entityType, type);
|
|
130
134
|
|
|
131
|
-
if (entityType !== "node") {
|
|
132
|
-
throw new Error("Governed translation create is implemented for nodes
|
|
135
|
+
if (entityType !== "node" && entityType !== "paragraph") {
|
|
136
|
+
throw new Error("Governed translation create is implemented for nodes and paragraphs.");
|
|
133
137
|
}
|
|
134
138
|
|
|
135
139
|
const backend = await resolveBackend(site);
|
|
@@ -137,46 +141,64 @@ async function createTranslation({
|
|
|
137
141
|
if (!existing) {
|
|
138
142
|
throw new Error("The entity was not found.");
|
|
139
143
|
}
|
|
140
|
-
const liveVid = entityRevisionId(existing);
|
|
141
|
-
const workingCopy = await loadWorkingCopy(backend, { entityType, bundle: type, id });
|
|
142
|
-
const workingVid = entityRevisionId(workingCopy);
|
|
143
|
-
const sameWorking = workingVid !== null && liveVid !== null && String(workingVid) === String(liveVid);
|
|
144
|
-
const draftRevision = {
|
|
145
|
-
liveVid,
|
|
146
|
-
workingVid: workingCopy && !sameWorking ? workingVid : undefined,
|
|
147
|
-
};
|
|
148
144
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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);
|
|
153
166
|
}
|
|
154
|
-
const drafted = await applySafeDraftDefault({
|
|
155
|
-
backend, entityType, bundle: type, id, attributes: safeAttributes, existingEntity: existing,
|
|
156
|
-
});
|
|
157
|
-
assertPublishAllowed(sec, drafted);
|
|
158
167
|
|
|
159
168
|
if (dryRun) {
|
|
160
169
|
await createTranslationDraft(backend, {
|
|
161
|
-
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, draftRevision,
|
|
170
|
+
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
162
171
|
}, true);
|
|
163
172
|
return {
|
|
164
173
|
dryRun: true, operation: "create_translation", entityType, bundle: type, id,
|
|
165
|
-
langcode: targetLang, attributes: drafted,
|
|
174
|
+
langcode: targetLang, attributes: drafted, ...(relationships ? { relationships } : {}),
|
|
166
175
|
};
|
|
167
176
|
}
|
|
168
177
|
|
|
169
178
|
const created = await createTranslationDraft(backend, {
|
|
170
|
-
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, draftRevision,
|
|
179
|
+
entityType, bundle: type, id, langcode: targetLang, attributes: drafted, relationships, draftRevision,
|
|
171
180
|
});
|
|
172
|
-
|
|
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
|
+
};
|
|
173
195
|
}
|
|
174
196
|
|
|
175
197
|
export const definitions = [
|
|
176
198
|
{
|
|
177
199
|
name: "drupal_list_translations",
|
|
178
200
|
description:
|
|
179
|
-
"List live and working translation langcodes for a Drupal node. Uses Sentinel's " +
|
|
201
|
+
"List live and working translation langcodes for a Drupal node or paragraph. Uses Sentinel's " +
|
|
180
202
|
"translation inventory when available (live default revision vs unpublished working " +
|
|
181
203
|
"draft). Core JSON:API alone serves one language and cannot prove others are absent. " +
|
|
182
204
|
"Defaults to node.",
|
|
@@ -193,23 +215,30 @@ export const definitions = [
|
|
|
193
215
|
{
|
|
194
216
|
name: "drupal_create_translation",
|
|
195
217
|
description:
|
|
196
|
-
"Create a translation as an unpublished non-default draft
|
|
218
|
+
"Create a translation as an unpublished non-default draft (governed write). " +
|
|
197
219
|
"Adds the target language beside the default language; it does not PATCH langcode on " +
|
|
198
|
-
"the canonical entity. English
|
|
199
|
-
"
|
|
200
|
-
"draft
|
|
201
|
-
"
|
|
202
|
-
"
|
|
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.",
|
|
203
230
|
inputSchema: {
|
|
204
231
|
type: "object", required: ["type", "id", "langcode"],
|
|
205
232
|
properties: {
|
|
206
|
-
site:
|
|
207
|
-
entityType:
|
|
208
|
-
type:
|
|
209
|
-
id:
|
|
210
|
-
langcode:
|
|
211
|
-
attributes:
|
|
212
|
-
|
|
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" },
|
|
213
242
|
},
|
|
214
243
|
},
|
|
215
244
|
},
|