@actuate-media/cms-core 0.52.0 → 0.53.0

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.
@@ -2511,13 +2511,25 @@ export function registerCMSRoutes(router) {
2511
2511
  }
2512
2512
  });
2513
2513
  // Shared: load a page document's sections, or a Response on error.
2514
- const loadPageSections = async (collection, id, ctx) => {
2514
+ //
2515
+ // ADR 0002 ownership guard: a document authored in the visual page builder
2516
+ // carries a `_layout` tree and its `sections` are *compiled* from that tree on
2517
+ // every save. Editing those sections directly would be silently overwritten by
2518
+ // the next builder save, so for WRITE callers we refuse a builder-owned doc
2519
+ // with 409 instead. Read callers are unaffected.
2520
+ const loadPageSections = async (collection, id, ctx, opts) => {
2515
2521
  if (!collectionSupportsSections(collection)) {
2516
2522
  return errorResponse(`Collection "${collection}" does not declare a "sections" JSON field, so it cannot store page sections.`, 400);
2517
2523
  }
2518
2524
  const doc = await getDocument(collection, id, ctx);
2519
2525
  if (!doc)
2520
2526
  return errorResponse('Page not found', 404);
2527
+ // `getDocument` lifts the stored `_layout` out of `data` onto the wire
2528
+ // envelope (`doc.layout`), so the builder-ownership signal lives there.
2529
+ const layout = doc.layout;
2530
+ if (opts?.forWrite && layout && typeof layout === 'object') {
2531
+ return errorResponse(`Page "${id}" is owned by the visual page builder: its sections are compiled from the builder layout and would be overwritten on the next save. Edit it in the Page Builder instead (or clear its layout to convert it to a section-authored page).`, 409);
2532
+ }
2521
2533
  const data = doc.data ?? {};
2522
2534
  return { sections: coerceSections(data.sections) };
2523
2535
  };
@@ -2572,7 +2584,7 @@ export function registerCMSRoutes(router) {
2572
2584
  return errorResponse(`Unknown section type "${sectionType}". Call GET /page-sections/types for valid ids.`, 400);
2573
2585
  }
2574
2586
  const ctx = buildActionContext(auth.session, db());
2575
- const loaded = await loadPageSections(params.collection, params.id, ctx);
2587
+ const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
2576
2588
  if (loaded instanceof Response)
2577
2589
  return loaded;
2578
2590
  const section = buildSection(sectionType, {
@@ -2614,7 +2626,7 @@ export function registerCMSRoutes(router) {
2614
2626
  if (!orderedIds)
2615
2627
  return errorResponse('orderedIds (string[]) is required', 400);
2616
2628
  const ctx = buildActionContext(auth.session, db());
2617
- const loaded = await loadPageSections(params.collection, params.id, ctx);
2629
+ const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
2618
2630
  if (loaded instanceof Response)
2619
2631
  return loaded;
2620
2632
  const next = reorderSectionList(loaded.sections, orderedIds);
@@ -2637,7 +2649,7 @@ export function registerCMSRoutes(router) {
2637
2649
  return scopeErr;
2638
2650
  const body = (await request.json().catch(() => null));
2639
2651
  const ctx = buildActionContext(auth.session, db());
2640
- const loaded = await loadPageSections(params.collection, params.id, ctx);
2652
+ const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
2641
2653
  if (loaded instanceof Response)
2642
2654
  return loaded;
2643
2655
  const sectionId = params.sectionId;
@@ -2682,7 +2694,7 @@ export function registerCMSRoutes(router) {
2682
2694
  if (scopeErr)
2683
2695
  return scopeErr;
2684
2696
  const ctx = buildActionContext(auth.session, db());
2685
- const loaded = await loadPageSections(params.collection, params.id, ctx);
2697
+ const loaded = await loadPageSections(params.collection, params.id, ctx, { forWrite: true });
2686
2698
  if (loaded instanceof Response)
2687
2699
  return loaded;
2688
2700
  const sectionId = params.sectionId;
@@ -8837,12 +8849,13 @@ export function registerCMSRoutes(router) {
8837
8849
  }
8838
8850
  const layout = await resolveLayout(pathParam, docData, matchedCollection);
8839
8851
  const { _layout: layoutTree, ...cleanData } = docData;
8840
- // WS-A5 compat bridge (ADR 0001): a document authored only in the
8841
- // page-builder tree (`_layout`, Model B) still renders through the
8842
- // canonical sections path. When it has no flat `sections` of its own,
8843
- // derive a best-effort PageSection[] from the tree at read time so
8844
- // page-builder content is never stranded. Non-destructive the stored
8845
- // document is untouched; only the response is enriched.
8852
+ // LEGACY fallback (ADR 0002): builder pages now compile `_layout` to
8853
+ // canonical `data.sections` on save, so this branch is a no-op for any
8854
+ // document written after that change. It still fires for LEGACY pages
8855
+ // authored in the tree before compile-on-save (and not yet migrated via
8856
+ // `migratePageBuilderSections`): when such a doc has no flat `sections`,
8857
+ // derive a best-effort PageSection[] at read time so page-builder content
8858
+ // is never stranded. Non-destructive — only the response is enriched.
8846
8859
  if ((!Array.isArray(cleanData.sections) || cleanData.sections.length === 0) &&
8847
8860
  layoutTree &&
8848
8861
  typeof layoutTree === 'object') {