Skip to content

Commit 777bc31

Browse files
committed
fix: findings from live smoke testing on Jahia 8.2
- Choicelist initializers receive the CONTENT locale (language being edited), not the editor UI locale — docs, TSDoc and Cypress spec corrected (verified against a live instance via the Content Editor forms API). - Actions guide: the render servlet writes the JSON body only when the request sends Accept: application/json.
1 parent 3b6c507 commit 777bc31

5 files changed

Lines changed: 130 additions & 7 deletions

File tree

‎docs/2-guides/4-actions/README.md‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,12 @@ The action is then reachable on any node URL:
3131

3232
```
3333
GET /cms/render/live/en/sites/mysite/home.myModuleGreet.do?who=Jahia
34+
Accept: application/json
3435
→ 200 {"greeting": "Hello Jahia", "path": "/sites/mysite/home"}
3536
```
3637

38+
Note that Jahia's render servlet only writes the JSON body when the request declares it accepts JSON — send an `Accept: application/json` header (browsers submitting forms get the redirect/status behavior instead).
39+
3740
## Declaration options
3841

3942
| Option | Description |

‎docs/2-guides/5-choicelist-initializers/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ The callback receives a context object:
4242
| Property | Description |
4343
|----------|-------------|
4444
| `param` | The parameter from the CND declaration: `choicelist[myModuleColors='myParam']` passes `"myParam"`. Empty string when absent. |
45-
| `locale` | BCP-47 language tag of the editing UI locale (e.g. `"en"`, `"fr"`). Use it to localize labels. |
45+
| `locale` | BCP-47 language tag of the content language being edited (e.g. `"en"`, `"fr"`) — not the editor's UI language. Use it to localize labels. |
4646
| `values` | Choices accumulated by previous initializers when several are chained in the CND declaration (e.g. `choicelist[resourceBundle,myModuleColors]`). Include them in your result to keep them. |
4747
| `node` | The node being edited, when it exists (it does not on creation forms). |
4848
| `java` | Escape hatch: the raw Java objects received by the underlying `ModuleChoiceListInitializer` — `propertyDefinition` (`ExtendedPropertyDefinition`), `locale` (`java.util.Locale`), `values`, `context`. |
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
---
2+
page:
3+
$path: /sites/academy/home/documentation/jahia/8_2/developer/javascript-module-development/migrations
4+
jcr:title: Writing Migrations
5+
j:templateName: documentation
6+
content:
7+
$subpath: document-area/content
8+
---
9+
10+
Migrations are run-once TypeScript scripts shipped with your module, executed when a new version of the module starts. They reconcile existing content with changes in your content type definitions — the JavaScript equivalent of Jahia's Groovy `META-INF/patches` scripts, with a typed API and built-in guard rails.
11+
12+
Typical uses: removing leftover values of a dropped property, backfilling a default on existing content, converting values after a property type change, deleting a retired node type, or renaming a node type and rebinding its content.
13+
14+
## Declaring a migration
15+
16+
Call `registerMigration` at the top level of a server file (it registers the migration as a side effect at module startup, like `registerAction`). Convention: one migration per file under `src/migrations/`:
17+
18+
```ts
19+
// src/migrations/2.0.0-01-remove-legacy-color.server.ts
20+
import { registerMigration } from "@jahia/javascript-modules-library";
21+
22+
registerMigration(
23+
{
24+
name: "2.0.0-01-remove-legacy-color",
25+
description: "color was dropped from mymodule:banner in 2.0.0 — clean leftover values",
26+
},
27+
({ migrate }) => {
28+
migrate.removePropertyValues({ nodeType: "mymodule:banner", property: "color" });
29+
},
30+
);
31+
```
32+
33+
The `name` is the migration's **run-once identity and ordering key**:
34+
35+
- A module's migrations run in lexicographic order of their names. Use the `"<moduleVersion>-<NN>-<slug>"` convention to keep them sorted.
36+
- Execution is recorded under the name in Jahia's module patch status store (`/module-management` → `j:bundlesScripts`, shared with Groovy patches). Whatever the outcome, a recorded migration **never runs again** — never rename or reorder a released migration; ship a new one instead.
37+
38+
Migrations run **synchronously** on the module start thread (like actions, they must not be `async`), on the **processing server only**, and by the time they run the module's new definitions are already registered.
39+
40+
## The `migrate.*` helpers
41+
42+
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).
43+
44+
```ts
45+
// U1 — remove a property + clean its values
46+
migrate.removePropertyValues({ nodeType: "mymodule:banner", property: "color" });
47+
48+
// U2 — add a property + backfill existing content
49+
migrate.setPropertyValues({
50+
nodeType: "mymodule:banner",
51+
property: "theme",
52+
onlyIfMissing: true, // default — never clobbers an existing value
53+
value: (node) => (node.getProperty("price").getDouble() > 1000 ? "premium" : "light"),
54+
});
55+
56+
// U3 — change a property's data type + convert values
57+
migrate.convertPropertyValues({
58+
nodeType: "mymodule:banner",
59+
property: "priority",
60+
convert: (value) => Number.parseInt(value.getString(), 10), // undefined = leave untouched
61+
});
62+
63+
// U4 — delete a definition (owned by this module)
64+
migrate.removeNodeType({
65+
nodeType: "mymodule:legacyBanner",
66+
ifContentExists: "delete", // default is "fail" — destroying content is opt-in
67+
});
68+
69+
// U5 — rename a definition + rebind existing items
70+
migrate.changeNodeType({
71+
from: "mymodule:oldBanner",
72+
to: "mymodule:banner", // must exist in the module's current definitions
73+
mapProperties: { legacyTitle: "title" },
74+
});
75+
```
76+
77+
Selection options on the bulk helpers: `scope` (limit to a subtree), `where` (a JCR-SQL2 constraint fragment), `includeSubtypes` (default `true`). Definition operations (`removeNodeType`, `changeNodeType`) only accept node types **owned by your module** — cross-module definition surgery is not supported.
78+
79+
## The imperative escape hatch
80+
81+
For everything else (ACL fixes, node moves, one-off repairs), open a system session or use the batching engine directly:
82+
83+
```ts
84+
registerMigration({ name: "2.0.0-02-fix-root-title" }, ({ jcr, log, skip }) => {
85+
jcr.withSystemSession({ workspace: "default" }, (session) => {
86+
const path = "/sites/mysite/contents/catalog-root";
87+
if (!session.nodeExists(path)) skip("nothing to fix on this instance");
88+
session.getNode(path).getRealNode().setProperty("jcr:title", null);
89+
session.save();
90+
log.info(`Cleaned stray jcr:title on ${path}`);
91+
});
92+
93+
jcr.forEachNode(
94+
{ query: "SELECT * FROM [mymodule:banner]", workspaces: ["default", "live"] },
95+
(node) => node.setProperty("migrated", true),
96+
);
97+
});
98+
```
99+
100+
With `locale: null` (the default), system sessions see translation subnodes as plain nodes — usually what migrations want.
101+
102+
## Outcomes and failure semantics
103+
104+
- Returning normally records `.installed`.
105+
- Calling `context.skip(reason)` records `.skipped`.
106+
- Throwing records `.failed`, and the module's **remaining migrations are halted** (they stay pending). The module itself still starts — migration failures never break startup.
107+
108+
All three outcomes are terminal. Batches already committed before a failure stay committed (JCR has no cross-save transactions), so write migrations to be **idempotent** — the built-in helpers are idempotent by construction.
109+
110+
## Development and testing
111+
112+
- `migrations.autoRun` (configuration PID `org.jahia.modules.javascript.modules.engine.migrations`, default `true`): set to `false` on development servers to log pending migrations at module start instead of running them.
113+
- The reliable test is an end-to-end one against a real Jahia: provision the previous module version, create content, deploy the new version, and assert the transformed content — see `tests/cypress/e2e/engine/migrationTest.cy.ts` in the javascript-modules repository for a complete example (statuses, all five operations, skip/failure semantics).
114+
- To re-run a migration on a development instance, remove its entry from the `j:bundlesScripts` property of `/module-management` (e.g. in the JCR browser) and restart the module. Dedicated CLI/GraphQL tooling for status, dry runs, and resets is planned.

‎javascript-modules-library/src/framework/registerChoiceListInitializer.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ export interface ChoiceListInitializerContext {
2121
* Parameter from the CND declaration `choicelist[myKey,'myParam']`; empty string when absent.
2222
*/
2323
param: string;
24-
/** BCP-47 language tag of the editing UI locale, e.g. `"en"` or `"fr-FR"`. */
24+
/**
25+
* BCP-47 language tag of the content language being edited (not the editor's UI language),
26+
* e.g. `"en"` or `"fr-FR"`.
27+
*/
2528
locale: string;
2629
/**
2730
* Choices accumulated by the previous initializers of the CND declaration chain (empty when this

‎tests/cypress/e2e/ui/choicelistInitializerTest.cy.ts‎

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,18 @@ const FORM_QUERY = `
3232
}
3333
`;
3434

35-
/** Fetches the creation form of the test node type and returns its fields, flattened. */
36-
const getFormFields = (uiLocale: string): Cypress.Chainable<Field[]> =>
35+
/**
36+
* Fetches the creation form of the test node type and returns its fields, flattened.
37+
* Initializers receive the CONTENT locale (the language being edited), not the UI locale.
38+
*/
39+
const getFormFields = (locale: string): Cypress.Chainable<Field[]> =>
3740
cy
3841
.apollo({
3942
query: FORM_QUERY,
4043
variables: {
4144
nodeType: "javascriptExample:testChoicelistInitializer",
42-
uiLocale,
43-
locale: "en",
45+
uiLocale: "en",
46+
locale,
4447
uuidOrPath: `/sites/${GENERIC_SITE_KEY}/home`,
4548
},
4649
})
@@ -101,7 +104,7 @@ describe("JS choicelist initializers", () => {
101104
});
102105
});
103106

104-
it("localizes labels through the ui locale", () => {
107+
it("localizes labels through the content locale", () => {
105108
getFormFields("fr").then((fields) => {
106109
expect(constraintLabel(field(fields, "color"), "red")).to.equal("Rouge");
107110
});

0 commit comments

Comments
 (0)