File

libs/nx/src/executors/shared/df-editor-server/update-dynamic-form-config.ts

Index

Properties

Properties

baseConfigDirectory (Optional)
Type string
configDirectory
Type string
import type {
  BuildingBlockConfiguration,
  DynamicFormBBConfiguration,
  DynamicFormConfiguration,
  Frame,
  PageConfiguration
} from '@allianz/taly-core/schemas';
import type { ApplicationJsonSchema, PagesJsonSchema } from '@allianz/taly-sdk';
import {
  getJsonFilePathInDirectory,
  getJsonFilePathsInDirectory,
  SPLIT_JOURNEY_PATHS
} from '@allianz/taly-sdk/node';
import { parse, stringify } from 'comment-json';
import { readFileSync, writeFileSync } from 'fs';
import { join } from 'path';

interface DfEditRequest {
  /**
   * When provided, the block is looked up inside that page's `blocks`. When omitted,
   * the block is looked up inside `frame.globalSidebar.tabs[].blocks` instead, since
   * the global sidebar is not page-scoped.
   */
  pageId?: string;
  buildingBlockId: string;
  config: DynamicFormConfiguration;
}

interface ConfigDirectories {
  configDirectory: string;
  baseConfigDirectory?: string;
}

/**
 * Updates the dynamic form configuration for a building block identified by `id`
 * directly in the source configuration file(s) on disk.
 *
 * Supports classic journeys (pages.json), split journeys (pages/*.json),
 * and multi-tenant journeys (tenant pages + base pages fallback).
 *
 * When `pageId` is omitted the block is treated as a global sidebar block and
 * looked up under `frame.globalSidebar.tabs[].blocks` in `pages.json(c)` (classic)
 * or `application.json(c)` (split / multi-tenant).
 *
 * Uses `comment-json` to preserve JSONC comments when writing back.
 */
export function updateDynamicFormConfig(
  { configDirectory, baseConfigDirectory }: ConfigDirectories,
  { pageId, buildingBlockId, config }: DfEditRequest
): void {
  // For multi-tenant: try tenant first, then fall back to base
  if (baseConfigDirectory) {
    const updatedInTenant = tryUpdateInDirectory(configDirectory, pageId, buildingBlockId, config);
    if (updatedInTenant) return;

    const updatedInBase = tryUpdateInDirectory(
      baseConfigDirectory,
      pageId,
      buildingBlockId,
      config
    );
    if (updatedInBase) return;

    throw new Error(
      `${notFoundLocation(
        buildingBlockId,
        pageId
      )} in tenant config "${configDirectory}" or base config "${baseConfigDirectory}".`
    );
  }

  const updated = tryUpdateInDirectory(configDirectory, pageId, buildingBlockId, config);
  if (updated) return;

  throw new Error(
    `${notFoundLocation(buildingBlockId, pageId)} in config directory "${configDirectory}".`
  );
}

function notFoundLocation(buildingBlockId: string, pageId: string | undefined): string {
  return pageId
    ? `Building block "${buildingBlockId}" not found in page "${pageId}"`
    : `Building block "${buildingBlockId}" not found in the configuration`;
}

/**
 * Tries to find and update the building block in the relevant config file(s) within a directory.
 * Routes to either page-block or global-sidebar lookup based on whether a pageId was given.
 * Returns true if the building block was found and updated.
 */
function tryUpdateInDirectory(
  configDirectory: string,
  pageId: string | undefined,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  if (pageId === undefined) {
    return tryUpdateInGlobalSidebar(configDirectory, blockId, newConfig);
  }

  // Classic journey: single pages.json(c) at the root
  const pagesFilePath = getJsonFilePathInDirectory(configDirectory, 'pages');
  if (pagesFilePath) {
    return updateBlockInClassicPagesFile(pagesFilePath, pageId, blockId, newConfig);
  }

  // Split journey: individual page files in pages/ subdirectory
  const pagesDir = join(configDirectory, SPLIT_JOURNEY_PATHS.FOLDERS.PAGES);
  const pageFiles = getJsonFilePathsInDirectory(pagesDir);
  for (const pageFile of pageFiles) {
    if (updateBlockInSplitPageFile(pageFile, pageId, blockId, newConfig)) {
      return true;
    }
  }

  return false;
}

/**
 * Tries to find and update a global sidebar block. The `frame.globalSidebar` config lives in
 * `pages.json(c)` for classic journeys and in `application.json(c)` for split/multi-tenant journeys.
 */
function tryUpdateInGlobalSidebar(
  configDirectory: string,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  const pagesFilePath = getJsonFilePathInDirectory(configDirectory, 'pages');
  if (pagesFilePath) {
    return updateGlobalSidebarBlockInFile<PagesJsonSchema>(pagesFilePath, blockId, newConfig);
  }

  const applicationFilePath = getJsonFilePathInDirectory(
    configDirectory,
    SPLIT_JOURNEY_PATHS.FILES.APPLICATION
  );
  if (applicationFilePath) {
    return updateGlobalSidebarBlockInFile<ApplicationJsonSchema>(
      applicationFilePath,
      blockId,
      newConfig
    );
  }

  return false;
}

/**
 * Updates a dynamic form block inside `frame.globalSidebar.tabs[].blocks` of the given file.
 * Works for both classic (`pages.json(c)`) and split/multi-tenant (`application.json(c)`)
 * journeys, since both share the same `frame` shape.
 */
function updateGlobalSidebarBlockInFile<T extends { frame?: Frame }>(
  filePath: string,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  const content = readAndParse<T>(filePath);
  const tabs = content.frame?.globalSidebar?.tabs;
  if (!tabs) return false;

  for (const tab of tabs) {
    if (updateBlockInBlocksArray(tab.blocks, blockId, newConfig)) {
      writeBack(filePath, content);
      return true;
    }
  }

  return false;
}

/**
 * Updates a dynamic form block in a classic pages.json(c) file.
 * The file has a top-level `pages` array, each with a `blocks` array.
 */
function updateBlockInClassicPagesFile(
  filePath: string,
  pageId: string,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  const content = readAndParse<PagesJsonSchema>(filePath);
  if (!content.pages) return false;

  const page = content.pages.find((p) => p.id === pageId);
  if (!page) return false;

  if (updateBlockInBlocksArray(page.blocks, blockId, newConfig)) {
    writeBack(filePath, content);
    return true;
  }

  return false;
}

/**
 * Updates a dynamic form block in a split page file.
 * Each file is a single page with a top-level `blocks` array.
 */
function updateBlockInSplitPageFile(
  filePath: string,
  pageId: string,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  const content = readAndParse<PageConfiguration>(filePath);
  if (!content.blocks || content.id !== pageId) return false;

  if (updateBlockInBlocksArray(content.blocks, blockId, newConfig)) {
    writeBack(filePath, content);
    return true;
  }

  return false;
}

/**
 * Searches for a block by id in the blocks array and updates its `form` property.
 * Only updates blocks that have a `form` property (dynamic form building blocks).
 * Returns true if a matching block was found and updated.
 */
function updateBlockInBlocksArray(
  blocks: (BuildingBlockConfiguration | DynamicFormBBConfiguration)[] | undefined,
  blockId: string,
  newConfig: DynamicFormConfiguration
): boolean {
  if (!blocks) return false;

  const block = blocks.find((b) => b.id === blockId);
  if (!block || !isDynamicFormBbConfig(block)) return false;

  block.form = newConfig;
  return true;
}

function isDynamicFormBbConfig(
  block: BuildingBlockConfiguration | DynamicFormBBConfiguration
): block is DynamicFormBBConfiguration {
  return Boolean((block as DynamicFormBBConfiguration).form);
}

function readAndParse<T>(filePath: string): T {
  const raw = readFileSync(filePath, 'utf8');
  return parse(raw) as T;
}

function writeBack(filePath: string, content: unknown): void {
  writeFileSync(filePath, stringify(content, null, 2) + '\n', 'utf8');
}

results matching ""

    No results matching ""