File

libs/nx/src/executors/shared/df-editor-server/df-editor-server.ts

Index

Properties

Properties

baseConfigDirectory (Optional)
Type string
configDirectory
Type string
port
Type number
import chalk from 'chalk';
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { createServer as createNetServer } from 'net';
import type { DynamicFormConfiguration } from '@allianz/taly-core/schemas';
import { updateDynamicFormConfig } from './update-dynamic-form-config';

export const DF_EDITOR_SERVER_DEFAULT_PORT = 4300;

interface DfEditPayload {
  pageId: string;
  buildingBlockId: string;
  config: DynamicFormConfiguration;
}

export interface DfEditorServerOptions {
  configDirectory: string;
  baseConfigDirectory?: string;
  port: number;
}

export async function startDfEditorServer(
  options: DfEditorServerOptions
): Promise<() => Promise<void>> {
  const server = createServer((req, res) => {
    setCorsHeaders(res);

    if (req.method === 'OPTIONS') {
      res.writeHead(204);
      res.end();
      return;
    }

    if (req.method === 'GET' && req.url === '/api/ping') {
      return handlePing(res);
    }

    if (req.method === 'POST' && req.url === '/api/dynamic-form') {
      return handleEditDynamicForm(req, res, options);
    }

    sendJson(res, 404, { error: 'Not found' });
  });

  await new Promise<void>((resolve, reject) => {
    server.on('error', (err: NodeJS.ErrnoException) => {
      if (err.code === 'EADDRINUSE') {
        console.error(
          chalk.red(
            `Dynamic Form editor server port ${options.port} is already in use. Please choose a different port or stop the process using it.`
          )
        );
      }
      reject(err);
    });

    server.listen(options.port, () => {
      console.log(`🚀 Dynamic Form editor server listening on http://localhost:${options.port}`);
      resolve();
    });
  });

  return () => closeServer(server);
}

function handlePing(res: ServerResponse): void {
  sendJson(res, 200, { status: 'ok' });
}

function handleEditDynamicForm(
  req: IncomingMessage,
  res: ServerResponse,
  options: DfEditorServerOptions
): void {
  parseJsonBody<DfEditPayload>(req)
    .then((body) => {
      if (!body.pageId || !body.buildingBlockId || typeof body.config !== 'object') {
        sendJson(res, 400, {
          error:
            'Payload must include "pageId" (string), "buildingBlockId" (string) and "config" (object).'
        });
        return;
      }

      try {
        updateDynamicFormConfig(
          {
            configDirectory: options.configDirectory,
            baseConfigDirectory: options.baseConfigDirectory
          },
          { pageId: body.pageId, buildingBlockId: body.buildingBlockId, config: body.config }
        );
        sendJson(res, 200, { success: true });
      } catch (err) {
        const message = err instanceof Error ? err.message : 'Unknown error';
        const isNotFound = message.includes('not found');
        sendJson(res, isNotFound ? 404 : 500, { error: message });
      }
    })
    .catch(() => {
      sendJson(res, 400, { error: 'Invalid JSON body.' });
    });
}

function parseJsonBody<T>(req: IncomingMessage): Promise<T> {
  return new Promise((resolve, reject) => {
    const chunks: Buffer[] = [];
    req.on('data', (chunk: Buffer) => chunks.push(chunk));
    req.on('end', () => {
      try {
        resolve(JSON.parse(Buffer.concat(chunks).toString()));
      } catch (err) {
        reject(err);
      }
    });
    req.on('error', reject);
  });
}

function sendJson(res: ServerResponse, statusCode: number, data: unknown): void {
  res.writeHead(statusCode, { 'Content-Type': 'application/json' });
  res.end(JSON.stringify(data));
}

function setCorsHeaders(res: ServerResponse): void {
  res.setHeader('Access-Control-Allow-Origin', '*');
  res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type');
}

function isPortAvailable(port: number): Promise<boolean> {
  return new Promise((resolve) => {
    const server = createNetServer();
    server.once('error', () => resolve(false));
    server.once('listening', () => {
      server.close(() => resolve(true));
    });
    server.listen(port);
  });
}

export async function findFreePort(): Promise<number> {
  const maxAttempts = 100;
  let port = DF_EDITOR_SERVER_DEFAULT_PORT;
  for (let attempt = 0; attempt < maxAttempts; attempt++) {
    if (await isPortAvailable(port)) {
      return port;
    }
    port++;
  }
  throw new Error(
    `Could not find a free port after ${maxAttempts} attempts (tried ${DF_EDITOR_SERVER_DEFAULT_PORT}–${
      port - 1
    }).`
  );
}

function closeServer(server: Server): Promise<void> {
  return new Promise((resolve, reject) => {
    server.close((err) => {
      if (err) {
        reject(err);
      } else {
        resolve();
      }
    });
  });
}

results matching ""

    No results matching ""