Skip to content

feat: content patches — run-once content transformations in JavaScript#697

Open
romain-pm wants to merge 1 commit into
mainfrom
feature/js-content-patches
Open

feat: content patches — run-once content transformations in JavaScript#697
romain-pm wants to merge 1 commit into
mainfrom
feature/js-content-patches

Conversation

@romain-pm

Copy link
Copy Markdown

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

registerContentPatch lets 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-level jcr.* access as an escape hatch.

registerContentPatch({ version: "1.0.0", name: "rename-legacy-color" }, ({ patch }) => {
  patch.forEachNode("SELECT * FROM [myMod:content] WHERE legacyColor IS NOT NULL", (node) => {
    node.setProperty("theme", node.getPropertyAsString("legacyColor"));
    node.getProperty("legacyColor").remove();
  });
});

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 from main.
  • Live-verified on Jahia 8.2.3, including the 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-bind are introduced here — they back the patch.*/jcr.* operations.

Verification

  • 12 registrar unit tests (ContentPatchRegistrarTest) + full engine-java suite green (29 tests)
  • Library tsc + publint, test-module build (content patches register in the server bundle)
  • Cypress spec contentPatchTest.cy.ts

Tracking

Closes #695.

…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.
@romain-pm
romain-pm requested a review from GauBen as a code owner July 23, 2026 13:08
Copilot AI review requested due to automatic review settings July 23, 2026 13:08
@github-actions

Copy link
Copy Markdown

📝 Documentation Guidelines

Thank 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 docs/ folder.

@github-actions

Copy link
Copy Markdown

🦜 Chachalog

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.

Create a new entry online or run npx chachalog@0.5.2 prompt to create a new entry locally.

romain-pm added a commit that referenced this pull request Jul 23, 2026
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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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-managementj: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

  • setPropertyValues passes locale.toString() into the user callback, which uses underscores for region locales (e.g. "en_US"). Using toLanguageTag() 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, changeNodeType correctly avoids unregistering, but it still logs "Node type … unregistered", which is misleading. Log a dry-run message when support.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() {

Comment on lines +83 to +87
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;
Comment on lines +168 to +170
support.unregisterNodeType(options.nodeType);
log.info(`Node type ${options.nodeType} unregistered`);
return report;
Comment on lines +79 to +99
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;
}
Comment on lines +40 to +42
## 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() {
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Content patches declared in JavaScript

2 participants