feat: content patches — run-once content transformations in JavaScript#697
feat: content patches — run-once content transformations in JavaScript#697romain-pm wants to merge 1 commit into
Conversation
…JavaScript Extracted from the JS server-extensions branch into its own PR for reviewability (part of EPIC #554). registerContentPatch declares versioned, run-once content transformations executed at module start, tracked by core's patch status store (persistent halt-on-failure barrier); guard-railed patch.* operations with low-level jcr.* access. Design: ADR-0006, CONTENT-PATCHES-PLAN.md. - ContentPatchRegistrar implements Registrar (engine SPI); no dependency on the extension-points mechanism, so this branches cleanly from main. - registerContentPatch + jcr/operations/types library API. - java-ts-bind: JCR mutation-method whitelist for the guard-railed operations. - Test-module fixtures (patchTest* CND types) + Cypress spec; registrar unit tests.
📝 Documentation GuidelinesThank you for contributing to our documentation! To ensure your contributions meet our standards, please review these resources:
This comment is posted automatically when changes are detected in the |
🦜 Chachalog
|
Content patches are an independent feature (ContentPatchRegistrar implements the engine Registrar SPI, no dependency on the extension-points mechanism). Moving them to #697 shrinks this PR by ~2300 LOC and separates two review audiences. This also neutralizes the mislabeled commit 3b6c507: the content-patch files it swept in are no longer part of this branch's net diff. Removed here: registerContentPatch + jcr/operations/types, ContentPatchRegistrar (+support, tests), the content-patch guide/ADR-0006/plan docs, the Cypress spec, and the patchTest* test-module fixtures. Left in place intentionally (not content-patch-exclusive, referenced by other verticals or generally useful): the org.json/test dependencies in the engine-java pom, the JCR mutation-method entries in .java-ts-bind, and JcrHelper.doExecuteAsSystem.
There was a problem hiding this comment.
Pull request overview
This PR introduces JavaScript/TypeScript “content patches”: versioned, run-once, ordered content transformations registered by JS modules and executed at module startup, with execution tracked in Jahia’s existing module patch status store.
Changes:
- Added a new library API (
registerContentPatch) with typed patch context (patch.*helpers +jcr.*escape hatch) and batching utilities. - Added a dedicated engine registrar (
ContentPatchRegistrar) that executes pending patches once per module start and records terminal results in/module-management→j:bundlesScripts. - Added end-to-end coverage via a test module fixture + Cypress assertions, and documented the feature (ADR + guide + plan/demo + changelog note).
Reviewed changes
Copilot reviewed 20 out of 20 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/cypress/e2e/engine/contentPatchTest.cy.ts | Cypress E2E spec asserting status-store results and transformed fixture content. |
| javascript-modules-library/src/index.ts | Exposes registerContentPatch and its public types from the library entrypoint. |
| javascript-modules-library/src/framework/contentPatches/types.ts | Defines the public TS types for patch declarations, context, operations, and JCR helpers. |
| javascript-modules-library/src/framework/contentPatches/registerContentPatch.ts | Implements patch registration into the server registry and sync-only execution adapter. |
| javascript-modules-library/src/framework/contentPatches/operations.ts | Implements guard-railed patch.* helpers (bulk property ops + node type changes/removal). |
| javascript-modules-library/src/framework/contentPatches/jcr.ts | Implements jcr.withSystemSession and jcr.forEachNode with batching + dry-run support. |
| javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java | Unit tests for ordering, once-only behavior, halting on failure, and status persistence. |
| javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchSupport.java | Java support object exposed to JS patches (logger, module metadata, dry-run, nodetype ops). |
| javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrar.java | New registrar that runs pending patches at bundle start and records outcomes. |
| javascript-modules-engine-java/src/main/java/org/jahia/modules/javascript/modules/engine/js/server/JcrHelper.java | Adds doExecuteAsSystem helper for system-session JCR mutations from JS. |
| javascript-modules-engine-java/pom.xml | Adds test/support dependencies needed by new code/tests (e.g., JSON, Mockito, GraalJS for tests). |
| javascript-modules-engine-java/.java-ts-bind/package.json | Extends the method whitelist to type JCR mutation APIs needed for content patches. |
| jahia-test-module/src/react/server/extensions/contentPatches.ts | Test-module patch fixtures exercising success/skip/fail/halt semantics and operations. |
| jahia-test-module/settings/definitions.cnd | Adds CND definitions used by the content patch fixtures. |
| docs/adr/README.md | Adds ADR index with content-patches ADR entry. |
| docs/adr/0006-javascript-content-patches.md | ADR documenting the design and operational semantics for JS content patches. |
| docs/2-guides/6-content-patches/README.md | User guide describing how to write and run content patches. |
| CONTENT-PATCHES-PLAN.md | Detailed implementation plan and verified core behavior notes. |
| CONTENT-PATCHES-DEMO.md | Contract/demo document showing intended developer experience and API shape. |
| .chachalog/js-content-patches.md | Release note entry for the new content patches feature. |
Comments suppressed due to low confidence (8)
javascript-modules-library/src/framework/contentPatches/operations.ts:108
setPropertyValuespasseslocale.toString()into the user callback, which uses underscores for region locales (e.g. "en_US"). UsingtoLanguageTag()avoids surprising locale formats and matches typical language-tag inputs.
const value = resolveValue(node, locale.toString());
javascript-modules-library/src/framework/contentPatches/operations.ts:232
- In dry-run mode,
changeNodeTypecorrectly avoids unregistering, but it still logs "Node type … unregistered", which is misleading. Log a dry-run message whensupport.isDryRun()is true.
if (options.removeOldDefinition ?? true) {
support.unregisterNodeType(options.from);
log.info(`Node type ${options.from} unregistered`);
}
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:125
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void alreadyRecordedContentPatchsNeverRunAgain() {
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:135
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void failedContentPatchsAreRecordedAsTerminal() {
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:145
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void failureHaltsTheModulesRemainingContentPatchs() {
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:169
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void clearingAFailedRecordReleasesTheHeldContentPatchs() {
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:200
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void autoRunDisabledLeavesContentPatchsPending() {
javascript-modules-engine-java/src/test/java/org/jahia/modules/javascript/modules/engine/registrars/contentpatches/ContentPatchRegistrarTest.java:233
- Typo in test method name: "ContentPatchs" → "ContentPatches".
public void modulesWithoutContentPatchsAreANoOp() {
| if (result && typeof (result as PromiseLike<unknown>).then === "function") { | ||
| throw new Error( | ||
| `Content patch returned a promise: content patches must be synchronous (do not use an async run function)`, | ||
| ); | ||
| } |
| } catch { | ||
| siteLocales = existingLocales(node); | ||
| } | ||
| return locales ? siteLocales.filter((l) => locales.includes(l.toString())) : siteLocales; |
| support.unregisterNodeType(options.nodeType); | ||
| log.info(`Node type ${options.nodeType} unregistered`); | ||
| return report; |
| NodeTypeRegistry registry = NodeTypeRegistry.getInstance(); | ||
| if (!registry.hasNodeType(name)) { | ||
| getLogger(bundle.getSymbolicName()).info( | ||
| "Node type {} is not registered on this instance, nothing to unregister", name); | ||
| return; | ||
| } | ||
| ExtendedNodeType type; | ||
| try { | ||
| type = registry.getNodeType(name); | ||
| } catch (NoSuchNodeTypeException e) { | ||
| return; // raced away, nothing to do | ||
| } | ||
| if (!bundle.getSymbolicName().equals(type.getSystemId())) { | ||
| throw new IllegalArgumentException("Node type " + name + " is owned by '" + type.getSystemId() | ||
| + "', not by this module ('" + bundle.getSymbolicName() | ||
| + "') — content patches may only remove their own definitions"); | ||
| } | ||
| if (dryRun) { | ||
| getLogger(bundle.getSymbolicName()).info("[dry-run] would unregister node type {}", name); | ||
| return; | ||
| } |
| ## The `migrate.*` helpers | ||
|
|
||
| Every helper iterates both the `default` and `live` workspaces (override with `workspaces`), commits in batches (`batchSize`, default 100), handles internationalized properties on their translation subnodes, logs progress, and no-ops gracefully when the node type was never registered on this instance (fresh installs). |
| javascript-modules: minor | ||
| --- | ||
|
|
||
| New: content patches — versioned, run-once content transformations declared in JavaScript with `registerContentPatch`, tracked by Jahia's patch status store so each runs exactly once per environment. Guard-railed operations (`patch.*`) with low-level `jcr.*` access as an escape hatch. |
| } | ||
|
|
||
| @Test | ||
| public void runsPendingContentPatchsInNameOrderAndRecordsResults() { |
Part of EPIC #554. Split out of #687 into its own PR for reviewability (the two features are independent — this branches cleanly from
main).What
registerContentPatchlets a JS module declare versioned, run-once content transformations executed at module start, tracked by Jahia's patch status store so each patch runs exactly once per environment (persistent halt-on-failure barrier). Guard-railed operations (patch.*) with low-leveljcr.*access as an escape hatch.Design & rationale: ADR-0006 (
docs/adr/0006-javascript-content-patches.md),CONTENT-PATCHES-PLAN.md.Notes for reviewers
ContentPatchRegistrar implements Registrar(the pre-existing engine SPI) — no dependency on the extension-points mechanism in feat: JS server extension points, TypeScript content patches, and the JSServerExtensionInvoker SDK #687, hence a standalone PR frommain.getRealNode().setPrimaryType()retype handling and property remapping around retypes (documented in the plan).JcrHelper.doExecuteAsSystem(system-session helper) and the JCR mutation-method entries in.java-ts-bindare introduced here — they back thepatch.*/jcr.*operations.Verification
ContentPatchRegistrarTest) + full engine-java suite green (29 tests)tsc+publint, test-module build (content patches register in the server bundle)contentPatchTest.cy.tsTracking
Closes #695.