You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
My biggest take away is that we should make this API in Java and expose it to both JS and Groovy scripts, because we only widen the gap with a JS-first implementation
While I agree there is a need for this it still doesn't make it easier on the CND-side to understand the migration process
This issue captures the analysis and re-layering plan: a small framework in Java (hosted in the javascript-modules engine for now, candidate for Jahia core in the long run), with bridges + tooling + autocomplete + samples for both Groovy and TypeScript. Implementation is deliberately out of scope here and will be discussed once the layering is agreed.
Batch engine: jcr.forEachNode — identifier snapshotting, per-batch sessions with save/refresh cycles, dry-run discard, per-workspace reports, query building from nodeType/scope/where
TypeScript — contentPatches/jcr.ts (~145 lines)
Language-agnostic domain logic
The five guard-railed operations: patch.removePropertyValues / setPropertyValues / convertPropertyValues / changeNodeType / removeNodeType, incl. i18n handling (translation subnodes, locale targeting) and the getRealNode().setPrimaryType() retype dance with property remapping around the retype
Not built — specified in CONTENT-PATCHES-PLAN.md §6
So the semantics are already in Java; what is JS-only is precisely the domain knowledge — the batching, i18n and retype know-how that a Groovy or Java developer must otherwise re-hand-roll (ScrollableQuery + getRealNode() + save/refresh boilerplate, as the reference Groovy scripts in visibility, templates-system, site-settings-seo and jcontent all do today, each slightly differently). Keeping it in TS is what "widens the gap".
For contrast, a Groovy module patch today gets exactly two bindings (log, setResult — core's GroovyPatcher) and raw APIs: no batching helper, no dry-run anywhere, no i18n helpers, no reports, and re-running one means hand-editing the j:bundlesScripts JSON.
Target layering
Java framework (in javascript-modules-engine for now; candidate for core later) — everything language-agnostic: the batch engine, the five operations, i18n handling, operation reports, definition guard rails, dry-run — joining the lifecycle/status pieces already there. Callbacks (value, convert, per-node visitors) become plain functional interfaces: Groovy closures and JS functions both coerce to those natively, so one API serves both languages without per-language logic.
Groovy bridge — Groovy patches keep their existing META-INF/patches lifecycle (extender + shared status store: nothing changes there); they gain the operations API, runnable samples, and an IDE autocomplete story (provided-scope dependency for editing; runtime access via OSGi service lookup, which is dynamic-dispatch-safe in Groovy).
TypeScript bridge — registerContentPatch keeps its exact published surface; patch.* / jcr.* become thin typed wrappers delegating to the Java operations, with the idiomatic TS façade preserved (object literals, unions, callbacks) over the java-ts-bind-typed calls.
What stays language-specific by design: the declaration/registration UX, the typed façades, samples, docs, and (JS side) the vite/CLI integration.
DevEx impact — the key assessment
JS module developers: no visible regression. The API surface of #697 (registerContentPatch, patch.*, jcr.*, reports, dry-run, skip semantics) is preserved by the TS façade, and the run-once/ordering/halt semantics don't move (already Java). Two things to hold the line on: the façade must stay idiomatic (hand-authored types over the generated binding, not raw java-ts-bind output), and error messages must cross the polyglot boundary as first-class messages. Side benefit: for constant-value operations, moving the loop into Java removes per-node JS↔Java crossings (today the TS loop crosses the boundary for every hasProperty/getProperty/setProperty call); only per-node callbacks still cross.
Java/Groovy module developers: the big win. They gain the guard-railed operations — batching, i18n, dry-run, fresh-install no-ops, reports — on the audit store they already use, replacing hand-rolled boilerplate that today varies script by script. This has immediate internal customers: the reference Groovy scripts inventoried in CONTENT-PATCHES-PLAN.md §2.2 each re-implement a subset of this, without dry-run and with known i18n traps.
One interim awkwardness to own: consuming an API exported by the JS engine bundle is a surprising dependency for a pure-Java module. Mitigation: isolate the API in a dedicated exported package with no engine/Graal types in its signatures (core-migration-ready by construction) and position it explicitly as engine-hosted-for-now. This is also the honest argument for the core move: the API's natural home is next to Patcher/BundleInfoJcrHelper, not in a rendering engine.
Ops/admin teams: one engine, one story. Same batching, logging and report format whatever the language; the planned tooling (GraphQL admin + CLI status/run/reset, dry-run by default) is built once on the Java framework and serves both ecosystems — including reset for Groovy patch records, replacing today's hand-editing of j:bundlesScripts.
The CND-side comprehension gap (second review point) becomes addressable language-neutrally instead of JS-only: a validate sweep (content-vs-definitions divergence report) in the Java framework serves Java and JS modules alike; a docs matrix mapping CND change → required operation (property dropped → removePropertyValues, type renamed → changeNodeType, …); later, CND-diff-aware scaffolding that suggests the patch skeleton. The inversion doesn't solve migration comprehension by itself — it is kept as an explicit workstream (P5) rather than assumed away.
Costs & risks
Rework of ~400 lines of proven TS into Java. Bounded: the registrar unit tests and the two-version Cypress e2e assert the public surface, which does not change — they are the safety net for a behavior-preserving move.
Polyglot interop seams: JS undefined vs Java null for "no value / leave untouched" sentinels, JS number → JCR long/double, exception unwrapping across the boundary (the flag-based skip() detection in feat: content patches — run-once content transformations in JavaScript #697 is the precedent that host-boundary exception identity can't be trusted). Each is known and testable.
API-stability pressure arrives earlier: an API consumed by Groovy scripts and Java modules is harder to iterate than a TS-internal one, and the core move later puts it under core's compatibility rules. The engine-hosted phase is exactly the time to shake the API out.
Options shape in Java: Map-friendly signatures (Groovy named args and JS object literals both map naturally) vs typed builders — pick what keeps both bridges idiomatic, not what is prettiest in Java.
Groovy static-import ergonomics: dynamic OSGi lookup is the guaranteed baseline; whether the patch-script classloader can also resolve the exported API statically needs a short spike (moot once core-hosted).
Scope of the Groovy bridge: operations-only (recommended — the extender lifecycle already provides run-once for Groovy patches), or eventually a declaration API for Java modules too? That is really the core-framework question, deferred to P6.
Plan (phases — implementation discussion comes later)
P0 — agree on this layering and settle open questions 1, 2 and 5.
P1 — extract the Java framework: batch engine + five operations + reports behind a language-neutral API (no Graal types in signatures), behavior-preserving against the existing tests.
P2 — TypeScript bridge: patch.*/jcr.* delegate to the Java API under the unchanged published types; existing unit + e2e suites green.
P4 — admin API (Content patches: admin GraphQL API — status, reset, run (P4) #728): GraphQL admin extension on the Java framework — status (incl. pending), reset, and, if the deferred-execution workflow is kept, run/runPending — covering JS and Groovy patch records, per CONTENT-PATCHES-PLAN.md §6.
Sub-issue of #695, following up on the tech-lead feedback in the #697 review (pullrequestreview-4916430758):
This issue captures the analysis and re-layering plan: a small framework in Java (hosted in the javascript-modules engine for now, candidate for Jahia core in the long run), with bridges + tooling + autocomplete + samples for both Groovy and TypeScript. Implementation is deliberately out of scope here and will be discussed once the layering is agreed.
Analysis — where the logic lives today (#697)
The branch already splits along the right line for semantics, but not for operations:
STARTEDhook, ordering, pending-set vs status store, halt-on-failure barrier,autoRun/dryRunconfigContentPatchRegistrar/module-management→j:bundlesScripts) via core'sBundleInfoJcrHelperunregisterNodeType, registered-type checks), per-patch logger, module metadata, dry-run flagContentPatchSupportJcrHelper.doExecuteAsSystemjcr.forEachNode— identifier snapshotting, per-batch sessions with save/refresh cycles, dry-run discard, per-workspace reports, query building fromnodeType/scope/wherecontentPatches/jcr.ts(~145 lines)patch.removePropertyValues/setPropertyValues/convertPropertyValues/changeNodeType/removeNodeType, incl. i18n handling (translation subnodes, locale targeting) and thegetRealNode().setPrimaryType()retype dance with property remapping around the retypecontentPatches/operations.ts(~260 lines)registerContentPatch, name validation, duplicate detection,skip()/sync contractstatus/run/reset/create)CONTENT-PATCHES-PLAN.md§6So the semantics are already in Java; what is JS-only is precisely the domain knowledge — the batching, i18n and retype know-how that a Groovy or Java developer must otherwise re-hand-roll (
ScrollableQuery+getRealNode()+ save/refresh boilerplate, as the reference Groovy scripts in visibility, templates-system, site-settings-seo and jcontent all do today, each slightly differently). Keeping it in TS is what "widens the gap".For contrast, a Groovy module patch today gets exactly two bindings (
log,setResult— core'sGroovyPatcher) and raw APIs: no batching helper, no dry-run anywhere, no i18n helpers, no reports, and re-running one means hand-editing thej:bundlesScriptsJSON.Target layering
javascript-modules-enginefor now; candidate for core later) — everything language-agnostic: the batch engine, the five operations, i18n handling, operation reports, definition guard rails, dry-run — joining the lifecycle/status pieces already there. Callbacks (value,convert, per-node visitors) become plain functional interfaces: Groovy closures and JS functions both coerce to those natively, so one API serves both languages without per-language logic.META-INF/patcheslifecycle (extender + shared status store: nothing changes there); they gain the operations API, runnable samples, and an IDE autocomplete story (provided-scope dependency for editing; runtime access via OSGi service lookup, which is dynamic-dispatch-safe in Groovy).registerContentPatchkeeps its exact published surface;patch.*/jcr.*become thin typed wrappers delegating to the Java operations, with the idiomatic TS façade preserved (object literals, unions, callbacks) over the java-ts-bind-typed calls.What stays language-specific by design: the declaration/registration UX, the typed façades, samples, docs, and (JS side) the vite/CLI integration.
DevEx impact — the key assessment
JS module developers: no visible regression. The API surface of #697 (
registerContentPatch,patch.*,jcr.*, reports, dry-run, skip semantics) is preserved by the TS façade, and the run-once/ordering/halt semantics don't move (already Java). Two things to hold the line on: the façade must stay idiomatic (hand-authored types over the generated binding, not raw java-ts-bind output), and error messages must cross the polyglot boundary as first-class messages. Side benefit: for constant-value operations, moving the loop into Java removes per-node JS↔Java crossings (today the TS loop crosses the boundary for everyhasProperty/getProperty/setPropertycall); only per-node callbacks still cross.Java/Groovy module developers: the big win. They gain the guard-railed operations — batching, i18n, dry-run, fresh-install no-ops, reports — on the audit store they already use, replacing hand-rolled boilerplate that today varies script by script. This has immediate internal customers: the reference Groovy scripts inventoried in
CONTENT-PATCHES-PLAN.md§2.2 each re-implement a subset of this, without dry-run and with known i18n traps.One interim awkwardness to own: consuming an API exported by the JS engine bundle is a surprising dependency for a pure-Java module. Mitigation: isolate the API in a dedicated exported package with no engine/Graal types in its signatures (core-migration-ready by construction) and position it explicitly as engine-hosted-for-now. This is also the honest argument for the core move: the API's natural home is next to
Patcher/BundleInfoJcrHelper, not in a rendering engine.Ops/admin teams: one engine, one story. Same batching, logging and report format whatever the language; the planned tooling (GraphQL admin + CLI
status/run/reset, dry-run by default) is built once on the Java framework and serves both ecosystems — includingresetfor Groovy patch records, replacing today's hand-editing ofj:bundlesScripts.The CND-side comprehension gap (second review point) becomes addressable language-neutrally instead of JS-only: a
validatesweep (content-vs-definitions divergence report) in the Java framework serves Java and JS modules alike; a docs matrix mapping CND change → required operation (property dropped →removePropertyValues, type renamed →changeNodeType, …); later, CND-diff-aware scaffolding that suggests the patch skeleton. The inversion doesn't solve migration comprehension by itself — it is kept as an explicit workstream (P5) rather than assumed away.Costs & risks
undefinedvs Javanullfor "no value / leave untouched" sentinels, JS number → JCR long/double, exception unwrapping across the boundary (the flag-basedskip()detection in feat: content patches — run-once content transformations in JavaScript #697 is the precedent that host-boundary exception identity can't be trusted). Each is known and testable.Open questions
content-patches-apiartifact; and a package name that survives the core move (rename + deprecated delegating façade, or pick-once).Plan (phases — implementation discussion comes later)
patch.*/jcr.*delegate to the Java API under the unchanged published types; existing unit + e2e suites green.CONTENT-PATCHES-PLAN.md§6.status/run/reset/createsugar over the admin API; content patches are fully supported without it.validatecontent-vs-definitions sweep, CND-change→operation docs matrix, CND-diff-aware scaffolding (candidate).Patcher/BundleInfoJcrHelper(and optionally inject it as a binding inGroovyPatcher, giving Groovy scripts zero-lookup access).